Реактивное программирование: история и контекст
Эта страница — о том, откуда взялась идея, на которой стоит Continuum: какую проблему она решает, кто и когда её формулировал, как она разветвилась на несколько традиций и почему мы выбрали самую старую и самую строгую из них.
Проблема: интерактивные системы ломают порядок исполнения
Обычная программа — это рецепт: шаг за шагом, сверху вниз, от входа к выходу. У неё есть начало и конец, и порядок исполнения совпадает с порядком текста.
Интерактивная система устроена иначе: она не заканчивается. Она сидит и ждёт — клика, клавиши, ответа сети, тика таймера — и должна реагировать на события, приходящие в любом порядке и в любой момент. Дэвид Харел и Амир Пнуэли в 1985 году назвали такие системы реактивными (reactive systems) и противопоставили их трансформационным (вход → вычисление → выход), заметив, что для реактивных систем классические способы описания программ подходят плохо.
Инженерный ответ по умолчанию — колбэки: «когда произойдёт X, выполни Y». Он работает, но на масштабе рождает то, что книга Блэкхита и Джонса о FRP называет шестью бедствиями обработчиков событий:
- непредсказуемый порядок — кто из слушателей сработает первым;
- пропущенное первое событие — подписались позже, чем оно случилось;
- грязное состояние — колбэк застал переменные полуобновлёнными;
- нити апдейтов — каскад «обработчик дёргает обработчик», который невозможно проследить;
- утечки слушателей — забытые отписки;
- случайная рекурсия — обработчик косвенно вызывает сам себя.
Каждое из бедствий — следствие одного и того же: состояние размазано по переменным, а его согласованность поддерживается вручную, кодом внутри колбэков. Реактивное программирование — это семейство попыток переложить согласованность на язык или библиотеку: вы объявляете зависимости, система поддерживает их истинными во времени.
Предыстория: потоки данных и электронные таблицы
Идея «программа — это граф зависимостей, а не список шагов» старше термина «реактивное программирование» на десятилетия.
- Dataflow-языки. Lucid (Эшкрофт и Уэйдж, 1976) описывал переменную не как ячейку, а как бесконечную последовательность значений во времени — по сути, прообраз Behavior.
- Электронные таблицы. VisiCalc (Бриклин и Фрэнкстон, 1979), затем Lotus 1-2-3 и Excel — первое массовое реактивное программирование: формула
=A1+B1— это объявленная зависимость, которую система поддерживает сама. Миллионы людей программировали реактивно, не зная этого слова. - Синхронные языки. Французская школа 1980-х — Esterel (Жерар Берри), Lustre (Каспи и Альбвакс, 1987), Signal — создавалась для бортовых систем (авионика Airbus, АЭС). Её ключевая идея — синхронная гипотеза: реакция на событие концептуально мгновенна и атомарна, время дискретно, «одновременно» — строго определённое понятие. Если это звучит как наши транзакции — так и есть: транзакция Continuum — прямой потомок этой идеи.
- Observer и MVC. В мире ООП та же потребность породила паттерн «наблюдатель» (Smalltalk MVC, конец 1970-х; канонизирован GoF в 1994). Это колбэки, дисциплинированные интерфейсом — но все шесть бедствий при нём остаются: статья «Deprecating the Observer Pattern» (Майер, Ромпф, Одерски, 2010) разбирает это по пунктам.
1997: FRP — Конал Эллиотт и Пол Худак
Functional Reactive Programming родился как статья «Functional Reactive Animation» (Конал Эллиотт, Пол Худак; ICFP 1997) и библиотека Fran на Haskell — для описания анимаций.
Вклад Fran — не «ещё одна библиотека событий», а денотационная семантика: точный математический смысл каждой конструкции.
Behavior a— значение в каждый момент времени: функцияTime → a. Причём время у Эллиотта непрерывное, как в физике — анимация описывается уравнением, а не циклом кадров.Event a— множество моментов со значениями:[(Time, a)](имя, которое Continuum позже переименовал вStream).
Композиция получает смысл алгебры: map, merge, switch, snapshot определены уравнениями, а не описанием реализации. Из этого следуют доказуемые свойства — в частности, отсутствие глитчей: полуобновлённое состояние не существует в модели, а не «редко наблюдается».
Эллиотт до сих пор настаивает, что FRP — это именно два свойства: денотационная семантика + непрерывное время. Всё остальное — «reactive programming», без «functional».
2000-е: развитие в Haskell и первые браузерные опыты
Чистый Fran имел практические проблемы (утечки времени/памяти при наивной реализации), и следующее десятилетие искало рабочие формулировки:
- Yampa (Нильссон, Кортни, Петерсон, ~2002) — «стрелочный» FRP: вместо сигналов первого класса — функции сигналов (
SF a b), что закрыло утечки ценой менее прямого стиля. На Yampa писали роботов и игры. - FrTime (Racket, ~2003) и Flapjax (2009, группа Кришнамурти в Брауне) — первые серьёзные попытки принести FRP в браузер; Flapjax был языком-надстройкой над JavaScript.
- Push-pull FRP (Эллиотт, 2009) — уточнение реализации: события «толкают» (push), поведения «тянут» (pull); ровно эта гибридная схема живёт в Continuum.
- reactive-banana (Апфельмус, ~2011) и Sodium (Стивен Блэкхит, ~2012) — зрелые библиотеки дискретного («классического») FRP: время не непрерывное, а последовательность транзакций; зато семантика строгая, реализация практичная, а библиотека переносима между языками (Java, C#, TypeScript…). Книга «Functional Reactive Programming» Блэкхита и Джонса (Manning, 2016) — учебник этой ветви и главный источник модели Continuum.
2009–2013: слово «reactive» уходит в народ
Параллельно академической линии термин «реактивное программирование» приобрёл два новых — и разных — значения.
Rx / ReactiveX. Эрик Мейер и команда в Microsoft выпустили Reactive Extensions (Rx.NET, 2009): IObservable<T> как дуал IEnumerable<T>, богатая алгебра операторов над потоками событий. RxJS принёс это в JavaScript, ReactiveX — почти во все языки. Rx гениален как библиотека асинхронных потоков, но это не FRP в смысле Эллиотта: нет Behaviors как отдельного понятия, нет транзакционной одновременности — глитчи в ромбе зависимостей возможны и являются известной ловушкой (combineLatest с общим предком).
Reactive Manifesto (2013, Йонас Бонер и др.) — третье значение слова: архитектура распределённых систем (отзывчивость, устойчивость, эластичность, обмен сообщениями). К программированию интерфейсов отношения почти не имеет; упоминаем, чтобы вы не путались в поиске.
2010-е: реактивность приходит в UI-фреймворки
- Knockout (2010) —
observableиcomputedс автоотслеживанием зависимостей в браузере; прямой предок сегодняшних сигналов. - Elm (Эван Чаплицки, диссертация 2012) — «конкурентный FRP» для GUI; показательно, что в 2016-м («A Farewell to FRP», Elm 0.17) Elm отказался от сигналов в пользу The Elm Architecture — редьюсер-цикла, который позже вдохновил Redux.
- React (Facebook, 2013) — слово «реактивный» в названии, но модель противоположная FRP: состояние меняется → компонент перезапускается → виртуальный DOM диффится. Реактивность здесь достигается пересчётом всего и сравнением, а не распространением по графу зависимостей.
- MobX (Вестстрате, 2015), реактивность Vue — «прозрачная» реактивность: чтение значения внутри вычисления автоматически создаёт зависимость (через прокси/геттеры). Удобно; цена — граф зависимостей неявный и динамический, о нём трудно рассуждать.
- Svelte 3 (2019) — реактивность средствами компилятора.
- SolidJS (Райан Карниато; 1.0 — 2021) — точечные сигналы (fine-grained) без виртуального DOM: компонент выполняется один раз, обновляются точечные привязки. Solid доказал индустрии жизнеспособность run-once-модели — и запустил «сигнальную волну»: Preact Signals (2022), Angular Signals (2023), предложение TC39 Signals для стандартизации в самом JavaScript (stage 1, 2024).
Две живые традиции — и где стоит Continuum
К 2020-м в UI осталось две основные традиции реактивности:
| Сигналы (Solid, Vue, Angular…) | Классический FRP (Sodium, Continuum) | |
|---|---|---|
| Зависимости | автоотслеживание при чтении | структура выражения (map/lift) |
| Граф | неявный, динамический | явный, статический по построению |
| Одновременность | батчинг как оптимизация | транзакции как семантика |
| События | нет отдельного понятия (эффекты) | Stream — первоклассное значение |
| Время | «текущее значение» | значение во времени; прошлое/настоящее различимы (hold) |
| Родословная | Knockout → MobX → Solid | Fran → Sodium → сюда |
Continuum сознательно выбирает вторую: Behaviors + Streams + транзакции из дискретного FRP (Sodium), run-once компоненты и точечные обновления DOM, чью практичность доказал Solid, и дерево владения для жизненного цикла. Непрерывное время Эллиотта тоже здесь — integral/warp для анимаций, с animationFrames() как дискретизацией.
Ставка простая: автоотслеживание удобнее писать, но явная алгебра проще читать и доказывать — а когда одновременность является семантикой, целые классы багов (глитчи, гонки чтения, «событие из прошлого») не нуждаются в дисциплине, чтобы не случаться. Они не выразимы.
2026: State и скоупы
В июле 2026 Continuum переименовал Behavior в Wire («провод»): имя из FRP-литературы честно указывало на родословную, но объясняло меньше, чем метафора провода, по которому всегда течёт текущее значение.
Метафора не пережила и месяца: вне проекта «провод» никто не узнавал, и в том же июле Wire стал State — словом, которым уже думают разработчики, приходящие из React, Vue и Svelte. Рассматривался и Signal, но был отклонён: у Continuum нет автоотслеживания зависимостей, и это имя обещало бы семантику Solid. Старые имена — Event/newEvent, Behavior, Wire/wire/WireSource — остаются депрекейтнутыми алиасами до 1.0.
Одновременно модель владения получила явную формулировку — «значения — формулы; состояние и эффекты принадлежат скоупу»: чистые производные вычисляются по спросу и спят без слушателей, а hold/accum/perform регистрируются у владельца — скоупа компонента или явного root() на уровне модуля.
Что читать
- Conal Elliott, Paul Hudak — Functional Reactive Animation (ICFP 1997) — первоисточник.
- Conal Elliott — Push-Pull Functional Reactive Programming (2009).
- Stephen Blackheath, Anthony Jones — Functional Reactive Programming (Manning, 2016) — модель, на которой стоит Continuum; наш конспект с отсылками к главам.
- David Harel, Amir Pnueli — On the Development of Reactive Systems (1985) — откуда слово «реактивный».
- Ingo Maier, Tiark Rompf, Martin Odersky — Deprecating the Observer Pattern (2010) — почему колбэки не чинятся дисциплиной.
- Evan Czaplicki — A Farewell to FRP (2016) — честный рассказ о том, почему Elm ушёл от сигналов.
- Ryan Carniato — статьи о точечной (fine-grained) реактивности (solidjs.com/blog) — сигнальная традиция из первых рук.
Незнакомый термин? Вся терминология этой документации объяснена в глоссарии.