Парадигмы программирования: карта, история и как они связаны
Слово «парадигма» в вакансиях и на собеседованиях обычно означает что-то вроде принадлежности к секте: «мы пишем в ООП», «а мы — функциональщики». Это самое бесполезное значение из возможных. Практический смысл другой: парадигма — это набор ограничений, которые вы добровольно накладываете на себя, чтобы взамен получить гарантии.
Каждая парадигма отбирает у вас какую-то свободу. Структурное программирование отобрало goto. ООП отобрало прямой доступ к чужим полям. Функциональное отобрало присваивание. Модель акторов отобрала разделяемую память. И каждый раз этот обмен окупался: из-за того, что вы не можете сделать что-то, целый класс ошибок становится невыразимым, а рассуждать о коде становится возможно локально — глядя на кусок, а не на всю программу.
Эта статья — вход в трек. Она отвечает на четыре вопроса: что такое парадигма строго, а не на уровне лозунгов; по каким осям парадигмы реально различаются; откуда они взялись и почему появлялись именно в таком порядке; и как они уживаются в одной кодовой базе — потому что в проде чистых парадигм не бывает.
Часть 1. Что такое парадигма на самом деле
Определение через ограничения, а не через возможности
Термин ввёл Роберт Флойд в Тьюринговской лекции 1978 года «The Paradigms of Programming», позаимствовав слово у Томаса Куна. Флойд говорил о парадигме как о способе видеть задачу: динамическое программирование, рекурсивный спуск, поиск с возвратом — для него это тоже парадигмы, только мелкого масштаба.
Самая полезная современная формулировка принадлежит Роберту Мартину («Clean Architecture», глава 3): парадигмы не добавляют возможностей, а отнимают их.
| Парадигма | Что отнимает | Что даёт взамен |
|---|---|---|
| Структурная | неограниченный goto |
декомпозицию на блоки, доказуемость по Хоару |
| Объектно-ориентированная | прямые указатели на функции и поля | инверсию зависимостей, полиморфные границы модулей |
| Функциональная | присваивание (мутацию) | ссылочную прозрачность, безопасную параллельность |
| Акторы / CSP | разделяемую память | отсутствие гонок по построению |
| Декларативная | управление порядком выполнения | оптимизацию и переносимость решения |
Проверьте эту оптику на себе: любая «продвинутая» техника, которую вы считаете полезной, почти наверняка формулируется как запрет. «Не мутируй входные аргументы», «не лезь в поля другого объекта», «не блокируй поток», «не смешивай I/O с логикой». Парадигма — это просто запрет, доведённый до уровня языка и системный настолько, что нарушить его сложнее, чем соблюсти.
Три вопроса, ответы на которые задают парадигму
Чтобы отличить парадигмы друг от друга без болтовни, достаточно спросить три вещи:
- Что является единицей композиции? Инструкция? Процедура? Объект? Функция? Правило? Поток событий? Тип?
- Где живёт состояние и кто имеет право его менять? Глобальная память? Поле объекта? Нигде (значения неизменяемы)? Внутри процесса, недоступного другим?
- Кто определяет порядок выполнения? Программист явно? Планировщик? Порядок данных? Механизм вывода (резолюция)? Граф зависимостей?
Всё остальное — синтаксис. Например, ООП и модель акторов очень похожи по ответу на (1) и (2) — это не случайность: Алан Кэй прямо говорил, что «ООП для меня означает только обмен сообщениями, локальное удержание и защиту состояния и позднее связывание» (письмо 2003 года). Различие между Java и Erlang — в ответе на (3): синхронный вызов метода против асинхронного сообщения.
Обратите внимание: ветви пересекаются. Реактивное программирование — декларативное по духу, но живёт поверх событий; акторы — императивные внутри и асинхронные снаружи. Это не дефект классификации, а свойство предмета: парадигмы не образуют разбиение, они образуют пространство с осями.
Все парадигмы вычислительно эквивалентны — и это важно
Тезис Чёрча — Тьюринга гарантирует: всё, что можно вычислить на машине Тьюринга (императивно), можно вычислить в лямбда-исчислении (функционально) и наоборот. Никакая парадигма не даёт вам новых вычислимых функций.
Значит, выбор парадигмы — это не про мощность, а про выразительность: сколько кода и какого качества нужно, чтобы выразить намерение, и что можно доказать о результате, не запуская его. Формально это уловил Маттиас Феллейзен в работе «On the Expressive Power of Programming Languages» (1991): язык A выразительнее языка B, если перевод из B в A локален (заменяем конструкцию на конструкцию), а обратный перевод требует глобальной перестройки программы. Именно поэтому добавление исключений или call/cc — это качественный скачок, а добавление синтаксического сахара — нет. Практический вывод: спорить «что мощнее, ООП или ФП» бессмысленно; осмысленный вопрос — «какие ошибки в этой конкретной задаче я хочу сделать невыразимыми».
Часть 2. История: почему парадигмы появлялись именно в таком порядке
История парадигм — это история борьбы со сложностью, где каждый следующий шаг решает проблему, созданную предыдущим успехом.
Логика этой ленты проста, если читать её как цепочку «проблема → ограничение»:
- Неструктурный императив быстро упирался в «код-спагетти». Ответ — теорема Бёма — Якопини (1966): любую программу можно выразить последовательностью, ветвлением и циклом. Дейкстра сделал из неё практический манифест.
- Структурный код перестал масштабироваться по данным: процедур много, глобальных структур тоже, кто кого меняет — неясно. Ответ — Simula/Smalltalk: связать данные с операциями и закрыть их. Так родилась инкапсуляция.
- ООП дало иерархии и связность, но принесло скрытое изменяемое состояние, размазанное по графу объектов. Стало больно в многопоточности. Ответ — ФП и акторы: либо запретить мутацию, либо запретить общий доступ.
- ФП и акторы решили корректность, но потребовали новых способов описывать «что должно быть». Ответ — декларативные DSL и реактивность: SQL, HTML, Terraform, потоки событий.
Джон Бэкус, автор Fortran, в Тьюринговской лекции 1977 года «Can Programming Be Liberated from the von Neumann Style?» сформулировал главный диагноз императивного стиля: программа — это последовательность присваиваний, проталкивающих слова через «бутылочное горлышко фон Неймана» между процессором и памятью, и потому мышление программиста тоже становится пословным. Через тридцать лет этот же аргумент вернулся, но уже по другой причине — многоядерность.
Часть 3. Одна задача — шесть парадигм
Абстракции проверяются кодом. Возьмём намеренно скучную задачу: есть список заказов; нужно вернуть суммарную выручку по клиентам, у которых заказов больше одного, отсортировав по убыванию суммы.
Императивно-процедурно (Python)
def revenue_by_repeat_customers(orders):
"""Явный цикл, явные аккумуляторы, явный порядок шагов."""
totals = {} # клиент -> сумма
counts = {} # клиент -> число заказов
for order in orders:
cid = order["customer_id"]
totals[cid] = totals.get(cid, 0) + order["amount"]
counts[cid] = counts.get(cid, 0) + 1
result = []
for cid, total in totals.items():
if counts[cid] > 1:
result.append((cid, total))
result.sort(key=lambda pair: pair[1], reverse=True)
return result
Единица композиции — процедура. Состояние — локальные словари, которые мутируются в цикле. Порядок задан программистом. Сложность: O(n) на проход + O(k log k) на сортировку, где k — число клиентов; память O(k).
Объектно-ориентированно (Python)
from dataclasses import dataclass
@dataclass
class CustomerAccount:
"""Состояние инкапсулировано: менять его можно только методом apply."""
customer_id: str
total: int = 0
order_count: int = 0
def apply(self, order) -> None:
self.total += order.amount
self.order_count += 1
@property
def is_repeat(self) -> bool:
return self.order_count > 1
class RevenueLedger:
def __init__(self) -> None:
self._accounts: dict[str, CustomerAccount] = {}
def register(self, order) -> None:
acc = self._accounts.setdefault(
order.customer_id, CustomerAccount(order.customer_id))
acc.apply(order) # позднее связывание: apply может быть переопределён наследником
def repeat_revenue(self) -> list[tuple[str, int]]:
repeats = (a for a in self._accounts.values() if a.is_repeat)
return sorted(((a.customer_id, a.total) for a in repeats),
key=lambda p: p[1], reverse=True)
Единица композиции — объект. Выигрыш не в скорости (та же O(n + k log k)), а в границе: RevenueLedger можно подменить другой реализацией, а правило «повторный клиент» живёт в одном месте. Цена — состояние размазано по объектам и мутабельно.
Функционально (Python, но в стиле ФП)
from functools import reduce
def revenue_fp(orders):
"""Ни одного присваивания в изменяемую структуру: только преобразования значений."""
def merge(acc, order):
cid = order["customer_id"]
prev_total, prev_count = acc.get(cid, (0, 0))
# возвращаем НОВЫЙ словарь: старый не трогаем
return {**acc, cid: (prev_total + order["amount"], prev_count + 1)}
aggregated = reduce(merge, orders, {})
return sorted(((cid, t) for cid, (t, c) in aggregated.items() if c > 1),
key=lambda p: -p[1])
Честное предупреждение: этот код — учебный. {**acc, ...} копирует словарь на каждом шаге, поэтому сложность деградирует до O(n·k) по времени и создаёт O(n·k) мусора. Именно из-за этого в настоящих функциональных языках используют не копирование, а персистентные структуры данных со структурным разделением (HAMT в Clojure и Scala), где «обновление» стоит O(log₃₂ n) ≈ константа и переиспользует почти всё дерево. Подробнее — в статье про структуры данных; теорию персистентности заложил Окасаки в «Purely Functional Data Structures». Мораль: парадигма не отменяет анализ сложности — наивная неизменяемость на плоских словарях это ловушка, а не добродетель.
Декларативно (SQL)
-- Мы описываем ЧТО хотим. Порядок соединений, выбор индексов,
-- параллелизм и стратегию агрегации выбирает планировщик.
SELECT customer_id, SUM(amount) AS total
FROM orders
GROUP BY customer_id
HAVING COUNT(*) > 1
ORDER BY total DESC;
Пять строк вместо пятнадцати — и на объёмах, не влезающих в память, СУБД сама выберет hash aggregate или sort aggregate, распараллелит скан и использует индекс. Плата известна каждому, кто ловил внезапный seq scan: вы не контролируете план и вынуждены влиять на него косвенно.
Логически (Prolog)
% Факты
order(o1, alice, 100).
order(o2, alice, 250).
order(o3, bob, 400).
% Правило: total(Customer, Total) истинно, если Total — сумма сумм заказов Customer
total(C, Total) :-
findall(A, order(_, C, A), Amounts),
Amounts = [_, _|_], % список длиннее одного элемента — повторный клиент
sum_list(Amounts, Total).
% Запрос: ?- total(C, T). → C = alice, T = 350.
Здесь нет ни цикла, ни вызова функции: мы описали отношение, а машина вывода ищет подстановки, делающие его истинным. Та же программа умеет работать «в обратную сторону» — свойство, которого нет ни у одной из предыдущих версий.
Реактивно / dataflow (TypeScript, RxJS-подобно)
// Поток заказов приходит во времени; результат — тоже поток, обновляющийся сам.
const repeatRevenue$ = orders$.pipe(
scan((acc, o) => {
const prev = acc[o.customerId] ?? { total: 0, count: 0 };
return { ...acc, [o.customerId]: { total: prev.total + o.amount, count: prev.count + 1 } };
}, {} as Record<string, { total: number; count: number }>),
map(acc => Object.entries(acc)
.filter(([, v]) => v.count > 1)
.map(([id, v]) => [id, v.total] as const)
.sort((a, b) => b[1] - a[1])),
distinctUntilChanged(shallowEqual), // не дёргаем подписчиков зря
);
Изменилась не логика, а модель времени: раньше вход был готовым списком, теперь — бесконечной последовательностью. Именно эта разница — «данные как значение» против «данные как поток» — отделяет ФП от реактивного программирования.
Часть 4. Оси, по которым парадигмы реально различаются
Одномерная картинка выше удобна, но упрощает. Полезнее держать в голове несколько независимых осей.
с длинным жизненным циклом?"} A -- "нет, это преобразование данных" --> F["Функциональное ядро
чистые функции, неизменяемые значения"] A -- "да" --> B{"К состоянию обращаются конкурентно?"} B -- "нет" --> O["ООП или процедурный модуль
инкапсуляция + инварианты"] B -- "да" --> C{"Нужна ли строгая изоляция отказов?"} C -- "да, падение части не должно ронять целое" --> ACT["Акторы
Erlang/Elixir, Akka"] C -- "нет, важнее пропускная способность" --> D{"Кто владеет данными?"} D -- "один владелец, передаём по каналу" --> CSP["CSP
goroutines + channels"] D -- "много читателей, редкие записи" --> STM["STM / персистентные структуры
Clojure, Haskell"] F --> R{"Данные приходят во времени?"} R -- "да" --> RX["Реактивное / dataflow"] R -- "нет" --> BATCH["Пакетная обработка
map/filter/reduce"] Q --> DEC{"Можно ли описать желаемый результат,
а не шаги?"} DEC -- "да" --> DSL["Декларативный DSL
SQL, Terraform, k8s, правила"] style F fill:none,stroke:#10b981,stroke-width:2px style ACT fill:none,stroke:#6366f1,stroke-width:2px style DSL fill:none,stroke:#8b5cf6,stroke-width:2px
Полный набор осей, по которым стоит классифицировать любую парадигму:
| Ось | Крайности | Что это меняет на практике |
|---|---|---|
| Состояние | изменяемое ↔ неизменяемое | возможность рассуждать локально, стоимость конкурентности |
| Эффекты | свободные ↔ явные в типе | тестируемость без моков, предсказуемость |
| Единица композиции | процедура / объект / функция / правило / поток | что вы переиспользуете и подменяете |
| Связывание | раннее (компиляция) ↔ позднее (рантайм) | скорость vs гибкость и расширяемость |
| Порядок выполнения | задан программистом ↔ выведен рантаймом | контроль производительности vs лаконичность |
| Типизация | статическая ↔ динамическая, номинальная ↔ структурная | какие ошибки ловятся до запуска |
| Модель времени | значение ↔ поток ↔ процесс | как выражается «то, что меняется» |
Полезная привычка: описывая архитектурное решение, называйте не парадигму, а позицию по этим осям. «Ядро — неизменяемое, эффекты явные, связывание позднее на границе портов» несёт информации в разы больше, чем «мы пишем в ФП-стиле». Свести оси к главному компромиссу помогает следующая карта:
Из квадранта видно главное: гарантии и контроль над машиной почти всегда обмениваются друг на друга, и единственная зона, где удаётся получить оба, — языки с продвинутыми системами типов вроде Rust, которые платят за это порогом входа и скоростью разработки. Никакой «лучшей» точки не существует; существует уместная точка для конкретного модуля.
Часть 5. Как парадигмы уживаются в одной кодовой базе
Чистых парадигм в проде нет. Java с 8-й версии функциональна, Haskell умеет мутировать в ST, Go даёт и структуры с методами, и каналы, а React объединил dataflow, неизменяемость и компонентную композицию. Вопрос не «какую выбрать», а «где провести границы».
Приём №1: функциональное ядро, императивная оболочка
Самый переносимый способ смешивать парадигмы, популяризованный Гэри Бернхардтом в докладе «Boundaries» (2012): вся логика — чистые функции над неизменяемыми данными; всё, что связано со временем, I/O и внешним миром, вытеснено в тонкий слой снаружи.
# ЯДРО: чистое. Никаких БД, часов, сети. Тестируется без моков.
from dataclasses import dataclass
@dataclass(frozen=True)
class Cart:
items: tuple[tuple[str, int], ...] = ()
promo: str | None = None
@dataclass(frozen=True)
class Priced:
total: int
commands: tuple[str, ...] # что оболочка должна сделать во внешнем мире
def price(cart: Cart, catalog: dict[str, int], is_weekend: bool) -> Priced:
"""Решение принимается здесь. Побочных эффектов нет — только новое значение."""
base = sum(catalog[sku] * qty for sku, qty in cart.items)
discount = 10 if (cart.promo == "WEEKEND" and is_weekend) else 0
total = base * (100 - discount) // 100
cmds = ("audit:priced",) + (("notify:discount",) if discount else ())
return Priced(total=total, commands=cmds)
# ОБОЛОЧКА: грязная и тупая. Только сбор фактов и исполнение команд.
def handle_checkout(request, db, clock, bus):
cart = db.load_cart(request.cart_id) # эффект: чтение
catalog = db.load_prices(sku for sku, _ in cart.items)
result = price(cart, catalog, clock.now().weekday() >= 5) # чистое решение
db.save_total(request.cart_id, result.total) # эффект: запись
for cmd in result.commands:
bus.publish(cmd) # эффект: публикация
return {"total": result.total}
Что здесь получилось: price тестируется таблицей входов-выходов без единого мока и переносится в другой рантайм целиком. handle_checkout содержит ноль условий, поэтому его почти нечем сломать. Ровно эта идея лежит в основе гексагональной архитектуры и портов-адаптеров.
Приём №2: разные парадигмы — разным слоям
Это не эклектика, а нормальная инженерия: у каждого слоя своя доминирующая проблема, и парадигма подбирается под неё. Ошибкой было бы обратное — тащить одну парадигму во все слои насильно (SQL-запросы, собранные из объектов ради «чистого ООП», или бизнес-правила, размазанные по монадическим трансформерам ради «чистого ФП»).
Приём №3: состояние как явный автомат
Трюк, который делает изменяемое состояние безопасным без отказа от него: описать его декларативно как конечный автомат, а переходы сделать чистой функцией.
TRANSITIONS = {
("draft", "submit"): "placed",
("placed", "payment_confirmed"): "paid",
("placed", "cancel"): "cancelled",
("paid", "warehouse_dispatched"): "shipped",
("shipped", "courier_confirmed"): "delivered",
}
def step(state: str, event: str) -> str:
"""O(1). Ошибка «оплатили отменённый заказ» невозможна структурно."""
try:
return TRANSITIONS[(state, event)]
except KeyError:
raise IllegalTransition(f"{state} -/-> {event}")
Здесь сошлись три парадигмы: таблица переходов — декларативная, step — чистая функция, а место хранения текущего состояния — императивное. Каждая делает то, что умеет лучше всего.
Часть 6. Типичные ошибки
Карго-культ ООП. Классы OrderManager, DataHelper, UtilsService — это процедурный код, переодетый в классы. Признак: у объекта нет инвариантов, только геттеры и сеттеры (анемичная модель). Инкапсуляция без инвариантов бесполезна.
ФП-фанатизм без структур данных. Копирование коллекций «ради неизменяемости» превращает O(n) в O(n²), как в примере выше. Неизменяемость окупается только вместе с персистентными структурами или с копированием на границе, а не в горячем цикле.
Наследование вместо композиции. Иерархии глубже двух уровней почти всегда оказываются неверными через год. Джошуа Блох («Effective Java», item 18) формулирует прямо: наследуйтесь только там, где отношение «является» устойчиво и класс спроектирован под наследование и задокументирован. Об этом же — весь трек принципов проектирования.
«Всё есть акторы» / «всё есть потоки». Если задача — синхронное вычисление, актор добавляет к ней сериализацию сообщений, потерю стек-трейсов и невозможность прочитать поток управления. Асинхронность — стоимость, которую платят за изоляцию и параллелизм, а не бесплатный бонус. Симметрично: декларативность там, где нужен контроль, работает ровно до момента, когда планировщик выберет неудачный план или движок правил зациклится — убедитесь, что вам есть чем контроль вернуть (индексы, подсказки, материализация).
Спор о парадигмах вместо разговора об ограничениях. «Давай сделаем функционально» — не аргумент. «Давай сделаем расчёт чистым, чтобы прогонять на нём property-based тесты и переиспользовать в батче» — аргумент.
Часть 7. Как это выглядит в проде
- WhatsApp на Erlang: два десятка инженеров и сотни миллионов пользователей. Работает не «магия ФП», а конкретное ограничение — процессы не разделяют память, поэтому падение одного не портит состояние других, а supervisor-дерево перезапускает поддерево. См. «Designing for Scalability with Erlang/OTP».
- React: UI как чистая функция состояния,
view = f(state). Именно неизменяемость позволяет сравнивать деревья по ссылке и делать эффективную реконсиляцию; хуки — это dataflow-граф зависимостей. - Kubernetes и Terraform: вы описываете желаемое состояние, контроллер выполняет цикл сверки. Это чистая декларативность плюс императивный reconciler внутри — та же схема «декларативное ядро, императивная оболочка», только на уровне инфраструктуры. См. трек data engineering.
- Spark и Kafka Streams: пользователь пишет dataflow-граф преобразований, движок сам решает, где шардировать, где переставить фильтр перед джоином. Rust: система владения — ограничение уровня парадигмы, запрещающее одновременное существование мутабельной ссылки и любой другой; плата за отсутствие гонок данных — порог входа.
- LINQ в C# (трек): один и тот же лямбда-код исполняется либо в памяти, либо транслируется в SQL — декларативное описание переносимо между исполнителями. Горутины и каналы в Go (трек): CSP как основной способ структурировать конкурентность в мейнстримном языке.
Общий знаменатель: в каждом случае выигрыш появился не от «стиля», а от конкретного отобранного права — на разделяемую память, на мутацию, на управление порядком выполнения.
Часть 8. Карта трека
Дальше трек идёт от самых старых парадигм к самым составным. Каждая статья самодостаточна, но читать по порядку выгоднее: аргументы следующей опираются на болевые точки предыдущей.
- Императивная и процедурная парадигмы — машина фон Неймана, структурное программирование, декомпозиция на процедуры, почему
gotoпроиграл. - Объектно-ориентированное программирование — инкапсуляция как защита инвариантов, полиморфизм как инверсия зависимостей, почему наследование переоценено.
- Функциональное программирование — чистота, ссылочная прозрачность, неизменяемость, композиция, работа с эффектами.
- Декларативное и логическое программирование — SQL, Datalog, Prolog, движки правил и системы ограничений.
- Конкурентные парадигмы — разделяемая память и блокировки, акторы, CSP, STM, их модели отказов.
- Реактивное программирование и dataflow — потоки как значения, backpressure, сигналы и вычисляемые значения.
- Обобщённое программирование и метапрограммирование — параметрический полиморфизм, шаблоны, макросы, кодогенерация, рефлексия.
- Аспектно-ориентированное и событийно-ориентированное — сквозная функциональность, перехват, шина событий, event sourcing.
- Как выбирать и смешивать парадигмы — практический разбор: границы, слои, миграции, антипаттерны смешивания.
Полезные соседние треки: паттерны проектирования (словарь ООП-решений), принципы проектирования (SOLID и что от него осталось), DDD (моделирование домена), Elixir (акторы и ФП в бою), TypeScript (структурная типизация и реактивность).
Мини-итог
- Парадигма — это дисциплина ограничений, а не набор возможностей: она отнимает право и взамен даёт гарантию. Все парадигмы вычислительно эквивалентны, поэтому выбор — про выразительность и проверяемость, а не про мощность.
- Различия сводятся к ответам на три вопроса: единица композиции, владелец состояния, распорядитель порядка выполнения.
- История парадигм — цепочка ответов на проблемы, порождённые предыдущим успехом: спагетти → структурность, глобальное состояние → инкапсуляция, скрытая мутация → чистота и изоляция, ручное управление → декларативность.
- В проде выигрывает не чистота, а границы: чистое ядро, тонкая императивная оболочка, декларативные DSL там, где движок умнее вас, и явные автоматы там, где состояние неизбежно.
- Прежде чем спорить о парадигме, назовите класс ошибок, который вы хотите сделать невыразимым. Если такого класса нет — спор беспредметен.
Источники
- Robert W. Floyd. The Paradigms of Programming, ACM Turing Award Lecture, 1978 — dl.acm.org
- John Backus. Can Programming Be Liberated from the von Neumann Style?, 1978 — dl.acm.org
- E. W. Dijkstra. Go To Statement Considered Harmful, CACM, 1968 — PDF
- Matthias Felleisen. On the Expressive Power of Programming Languages, 1991 — ScienceDirect
- Peter Van Roy, Seif Haridi. Concepts, Techniques, and Models of Computer Programming — лучшая книга о парадигмах как о едином пространстве; постер с картой парадигм
- Robert C. Martin. Clean Architecture, гл. 3–6 (парадигмы как ограничения); Chris Okasaki. Purely Functional Data Structures (цена неизменяемости)
- C. A. R. Hoare. Communicating Sequential Processes, 1978 — PDF; Carl Hewitt et al. A Universal Modular ACTOR Formalism, 1973
- Alan Kay о том, что он имел в виду под ООП — письмо, 2003
- Gary Bernhardt. Boundaries — destroyallsoftware.com; Martin Fowler, AnemicDomainModel
Что дальше
Начнём с фундамента, поверх которого выросло всё остальное, — с модели вычислений, которая буквально повторяет устройство железа, и с дисциплины, которая сделала её пригодной для больших программ: Императивная и процедурная парадигмы.