Теория языков программирования
Парадигмы программирования
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 | Как должен выглядеть UI | Virtual 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, не осознавая этого.
| Особенность | Prolog | SQL | Datalog |
|---|---|---|---|
| Тип | Общего назначения | Запросы к БД | Запросы к БД |
| Факты | 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