Теория языков программирования

Парадигмы программирования

Python, JavaScript, Go, Rust, Haskell - это не разные инструменты. Это разные ответы на один вопрос: как управлять сложностью? Парадигма - это не синтаксис. Это модель вычисления. Rust ownership - это linear types из теоретической работы 1970-х. Haskell monads - это категорная теория 1958 года. Иммутабельность в React - это те же идеи, которые Джон Маккарти заложил в Lisp в 1958-м. Всё уже было изобретено - просто переименовано и переупаковано. Понять пять парадигм значит перестать удивляться каждому новому языку и начать видеть, какую старую идею он переоткрывает.

  • **Rust** = linear types (1970-е) + affine type theory. Borrow checker - это не новая идея, это формальная система из академических статей, наконец ставшая mainstream. Zero-cost abstractions обрабатывают пакеты в Cloudflare на 1 млн RPS
  • **React/Redux** - это функциональная парадигма: иммутабельный state + чистые редьюсеры. Идеи из Haskell и ML, упакованные в JavaScript. Используется в продуктах с 100M+ пользователями
  • **Apache Spark** использует функциональную модель (map/flatMap/reduce) для обработки петабайт данных. В императивном стиле та же задача стала бы кошмаром синхронизации
  • **SQL** - это реляционное исчисление Кодда 1970 года + логическая парадигма Пролога. Каждый SELECT - логический запрос, оптимизатор решает «как» вместо программиста

Императивное программирование: рецепт для компьютера

Ада Лавлейс в 1843 году писала первую программу для аналитической машины Бэббиджа. Там не было абстракций - только список арифметических операций по порядку. Это и есть **императивное программирование**: компьютеру даётся **последовательность команд**, каждая из которых изменяет состояние памяти. За 180 лет синтаксис изменился, модель вычисления - нет.

**Императивная парадигма** - стиль программирования, в котором программа представляет собой **последовательность команд**, изменяющих состояние. Ключевые элементы: **переменные** (ячейки памяти), **присваивание** (изменение состояния), **циклы** (повторение), **ветвление** (условия). Программа - это рецепт: шаг 1, шаг 2, шаг 3...

ПлюсыМинусы
Понятно новичкам - похоже на бытовые инструкцииБаги мутации: кто и когда изменил переменную?
Полный контроль над каждым шагомСложно распараллеливать - шаги зависят друг от друга
Эффективно по памяти - изменяем на местеКод разрастается: 10 строк логики → 50 строк управления
Близко к железу - процессор работает императивноТрудно формально доказать корректность

От Ады Лавлейс до Go

Первую программу в истории написала Ада Лавлейс в 1843 году - для аналитической машины Чарльза Бэббиджа. Эта программа была чисто императивной: последовательность арифметических операций для вычисления чисел Бернулли. С тех пор линия не прерывалась: ассемблер (1950-е), FORTRAN и COBOL (1957), C (1972), Go (2009). Даже «современные» языки вроде Go сознательно выбирают императивную простоту.

**Мутация - главный источник багов.** Когда 5 функций изменяют одну переменную, найти, кто сломал данные, становится детективной задачей. Именно поэтому появились другие парадигмы - они пытаются решить проблему неконтролируемого изменения состояния.

Какая ключевая черта отличает императивное программирование от других парадигм?

Декларативное программирование: что, а не как

Эдгар Кодд в 1970 году придумал реляционную алгебру - и вместе с ней идею, которая переизобретается снова и снова: описывай ЧТО нужно, не КАК это найти. SQL - прямое следствие. React - то же самое для UI. Terraform - для инфраструктуры. **Декларативный** подход работает потому, что снимает с программиста задачу оптимизации: SQL-оптимизатор переберёт 50 планов выполнения, человек - 2.

**Декларативная парадигма** - стиль, в котором программист описывает **желаемый результат**, а система сама определяет, как его достичь. Описываются СВОЙСТВА решения, а не шаги. SQL, HTML, CSS, регулярные выражения, React, Terraform - всё это декларативные инструменты.

  • Императивный DOM (JavaScript) — const div = document.createElement('div'); div.className = 'card'; const h2 = document.createElement('h2'); h2.textContent = user.name; div.appendChild(h2); container.appendChild(div); // 6 строк ручных манипуляций
  • Декларативный UI (React) — <div className="card"> <h2>{user.name}</h2> </div> // Описываем ЧТО показать, React сам обновит DOM

**Regex** - ещё один декларативный инструмент. Выражение `^[A-Z][a-z]+$` описывает паттерн: «начинается с заглавной, далее строчные». Цикл с проверкой каждого символа писать не нужно - движок regex сам решает, как искать. HTML описывает **структуру** страницы. CSS описывает **внешний вид**. Terraform описывает **инфраструктуру**. Везде одна идея: описание, не инструкция.

**Декларативный код легче читать и поддерживать**, потому что он ближе к формулировке задачи. SQL-запрос читается почти как английское предложение: «ВЫБЕРИ имя, зарплату ИЗ сотрудников ГДЕ отдел = Engineering». Императивный эквивалент требует прочитать каждый шаг, чтобы понять цель.

ИнструментЧто описываетКто решает «как»
SQLКакие данные нужныОптимизатор СУБД
HTMLСтруктуру документаБраузерный движок
CSSВнешний вид элементовLayout engine
React JSXКак должен выглядеть UIVirtual DOM reconciler
RegexПаттерн строкиRegex engine (NFA/DFA)
TerraformЖелаемое состояние инфраструктурыTerraform provider

Какой из этих инструментов НЕ является декларативным?

Функциональное программирование: функции как кубики LEGO

Джон Маккарти создал Lisp в 1958 году на основе лямбда-исчисления Чёрча. Ключевая идея: функция - это математический объект, а не процедура. f(x) = x² **всегда** даёт одно и то же для одного x. Никаких зависимостей от внешнего мира. Никаких побочных эффектов. Такие функции называются **чистыми** - и вся функциональная парадигма строится на этом единственном ограничении, дающем огромный профит при параллелизации.

**Функциональное программирование (FP)** строится на трёх столпах: 1. **Чистые функции** - результат зависит только от аргументов, нет побочных эффектов. 2. **Иммутабельность** - данные не изменяются, создаются новые копии. 3. **Функции как first-class citizens** - функции можно передавать как аргументы, возвращать из функций, хранить в переменных.

**Почему FP упрощает тестирование?** Чистую функцию тестировать предельно прямо: передай вход → проверь выход. Не нужны моки, стабы, настройка окружения. А **иммутабельность** убирает целый класс багов: если данные не меняются, невозможен race condition при параллельном доступе.

**map, filter, reduce** - три мушкетёра FP. `map` преобразует каждый элемент. `filter` оставляет подходящие. `reduce` сворачивает коллекцию в одно значение. Практически любой цикл можно переписать комбинацией этих трёх функций.

Функциональное программирование - это отказ от циклов ради непонятных map и reduce

FP - это способ мышления: разбить задачу на маленькие чистые функции и комбинировать их как кубики LEGO. map/filter/reduce - инструменты, а не самоцель. Главная ценность - предсказуемость и тестируемость кода

Чистая функция - это контракт: только аргументы влияют на результат. Это делает код тестируемым без моков и параллелизуемым без мьютексов. Apache Spark, JAX, React - все используют эту идею в промышленном масштабе. Цикл for не запрещён - просто каждый цикл с мутацией это потенциальный race condition в многопоточной системе.

Функция readFile(path) читает файл с диска и возвращает его содержимое. Является ли она чистой?

ООП: объекты как маленькие миры

Алан Кей создавал Smalltalk в 1972 году, вдохновляясь биологией: клетки организма инкапсулируют состояние и общаются через химические сигналы. Объект в его модели - не контейнер данных, а **автономный агент**: он получает сообщения и сам решает, как реагировать. Данные и поведение неразделимы. Именно эту идею переосмыслили C++ и Java - и Кей потом сожалел, что его идею так поняли.

**ООП** объединяет данные и поведение в **объекты**. Четыре принципа: 1. **Инкапсуляция** - данные скрыты, доступ через методы. 2. **Наследование** - новый класс наследует свойства существующего. 3. **Полиморфизм** - один интерфейс, разные реализации. 4. **Абстракция** - выделение существенного, сокрытие деталей.

Алан Кей и «посылка сообщений»

Алан Кей, создатель Smalltalk (1972), придумал термин «объектно-ориентированное программирование». Но позже он сожалел: «Я придумал термин ООП, и могу сказать, что не имел в виду C++». Для Кея главным было не наследование, а ПОСЫЛКА СООБЩЕНИЙ между объектами - как клетки в организме общаются через химические сигналы. Объект не вызывает метод - он отправляет сообщение, а получатель сам решает, как ответить.

**Наследование - не главная идея ООП.** Многие путают ООП с глубокими иерархиями классов. В реальности опытные разработчики используют наследование редко. Принцип «Composition over Inheritance» (Gang of Four, 1994) гласит: собирайте поведение из компонентов, а не наследуйте от предков.

Что Алан Кей считал главной идеей ООП?

Логическое программирование: факты, правила, выводы

Алан Колмероэр создал Prolog в 1972 году на основе формальной логики первого порядка. Радикальная идея: программа - это не алгоритм, а **база знаний**. Факты и правила. Запрос - и система сама строит доказательство через унификацию и бэктрекинг. Никакого «как искать» - только «что истинно». Та же модель, что в SQL, только мощнее: рекурсия, вывод, реляционные запросы с автоматическим выводом типов.

**Логическое программирование** основано на формальной логике. Программа - это набор **фактов** (утверждений о мире) и **правил** (логических связей). **Prolog** (1972) - главный логический язык. Программист описывает ЧТО ИСТИННО, а система использует **унификацию** и **бэктрекинг** для поиска ответов.

**SQL - тоже логическое программирование!** `SELECT * FROM users WHERE age > 18 AND city = 'Moscow'` - это логический запрос: «найди все записи, для которых ИСТИННО, что age > 18 AND city = Moscow». Datalog - язык запросов к базам данных, основанный на Prolog. Многие разработчики используют логическое программирование ежедневно через SQL, не осознавая этого.

ОсобенностьPrologSQLDatalog
ТипОбщего назначенияЗапросы к БДЗапросы к БД
Фактыparent(tom, bob).INSERT INTO ...parent(tom, bob).
Правилаancestor(X,Y) :- ...VIEW / подзапросancestor(X,Y) :- ...
Запрос?- ancestor(tom, X).SELECT ... WHERE ...?- ancestor(tom, X).
ПоискБэктрекингОптимизаторФиксированная точка
ПрименениеAI, эксперт. системыВсе СУБДАнализ программ

Логическое программирование - экзотика из 80-х, которая не используется в реальных проектах

SQL - самый популярный в мире логический язык. Constraint solvers работают в Google OR-Tools, SAT-решателях для верификации микросхем, в Datalog для анализа кода (инструмент Souffle в Facebook). Логическая парадигма живёт и процветает, просто не всегда под именем Prolog

Datalog - логический язык, используемый в Datomic (база данных Рича Хикки), в статическом анализе кода (Semmle/CodeQL, купленный GitHub). Каждый запрос в CodeQL - это логический вывод по AST кода. Millions of GitHub repos проверяются через логическую парадигму ежедневно. Prolog экзотичен, идея - нет.

В Prolog-программе с фактами parent(tom, bob) и parent(bob, alice), и правилом ancestor(X,Y) :- parent(X,Z), ancestor(Z,Y). Что вернёт запрос ?- ancestor(tom, alice)?

Ключевые идеи

  • **Парадигма - это модель вычисления, не синтаксис.** Python может быть императивным, функциональным и ООП-шным - в одном файле
  • **Императивный**: шаги + мутация. Понятно машине, опасно для человека - мутация порождает 70% багов в многопоточном коде
  • **Декларативный + логический**: описываешь ЧТО, система решает КАК. SQL-оптимизатор может переписать запрос 50 способами - программист не мог бы
  • **Функциональный**: чистые функции - это мечта тест-инженера. Нет побочных эффектов = нет зависимости от порядка выполнения = параллелизм бесплатно
  • **ООП**: настоящая идея Кея - посылка сообщений, не иерархии. Gang of Four в 1994 сказали «предпочитайте композицию наследованию» - 30 лет спустя это всё ещё игнорируют
  • **Rust ownership, Haskell monads, React reducers** - всё это старые академические идеи с новыми именами. Понять парадигмы - значит читать между строк любого нового языка

Связанные темы

Парадигмы программирования - отправная точка для глубокого изучения теории языков:

  • Системы типов — Каждая парадигма по-разному использует типы: ООП - nominal typing, FP - algebraic data types, логическое - термы
  • Лямбда-исчисление — Математический фундамент функционального программирования: любая вычислимая функция выразима через λ-абстракцию
  • Компиляторы — Компилятор транслирует декларативный или функциональный код в императивные машинные инструкции - мост между парадигмами

Вопросы для размышления

  • В типичном современном проекте сочетаются как минимум две парадигмы. Какие именно появляются чаще всего и почему?
  • При проектировании навигатора GPS какую парадигму выбрать для каждой части: поиск маршрута, отрисовка карты, обработка голосовых команд?
  • Почему большинство современных языков (Python, Kotlin, Rust, Swift) - мультипарадигменные? Может ли «чистая» парадигма покрыть все потребности реального проекта?

Связанные уроки

  • comp-01-intro — Понимание парадигм помогает разобраться, как компиляторы обрабатывают разные стили кода
  • plt-02-type-systems — Системы типов строятся поверх парадигм: функциональные → вывод типов, ОО → полиморфизм
  • fl-01-intro — Формальные языки объясняют математические основания логической и функциональной парадигм
  • dm-01 — Логическое программирование (Prolog) - это исполняемая дискретная математика
  • comp-04-interpreter-vs-compiler
Парадигмы программирования

0

1

Войти