Skip to content

Что такое FRP — на пальцах

Continuum построен на «функциональном реактивном программировании» (FRP). Звучит страшно. На деле вы им уже пользовались — если хоть раз открывали Excel. Эта страница объясняет всю идею без единого умного слова.

Вы уже это умеете: Excel

Впишите в ячейку C1 формулу =A1+B1. Теперь поменяйте A1 — и C1 обновилась сама. Вы не писали «когда меняется A1, пересчитай C1 и не забудь про D4, который зависит от C1». Таблица сама знает, что от чего зависит, и сама держит всё в актуальном состоянии.

Вот это и есть реактивное программирование. Всё. Ядро идеи — одна фраза:

Вы объявляете, что от чего зависит, а система сама следит, чтобы это всегда было правдой.

А как без этого живёт обычный код

В обычном JavaScript переменные — мёртвые ячейки. Поменяли одну — остальные об этом не узнают:

js
let a = 1;
let c = a + 1; // c = 2
a = 5; // а c так и осталась 2. Никто ей не сообщил.

Поэтому весь классический UI-код — это ручная рассылка новостей: «кнопку нажали → обнови счётчик → и текст → и не забудь кнопку „Отправить" задизейблить». Каждое «не забудь» — будущий баг: забыли одно место — и страница показывает враньё.

React решает это по-своему: «не будем ничего рассылать — просто перерисуем весь компонент заново и сравним, что изменилось». Работает, но приходится бороться с последствиями перерисовок: мемоизация, массивы зависимостей, устаревшие замыкания.

FRP решает как Excel: не перерисовывать, а связать.

Три слова, которые нужно знать

Реактивное значение (в коде — State; в FRP-литературе такой тип называют Behavior). Это ячейка Excel: у неё всегда есть текущее значение, и все, кто от неё зависит, обновляются сами. Текст поля ввода, счётчик, текущий пользователь.

ts
const count = state(0); // ячейка со значением 0

Производное значение — формула. Не хранится, а вычисляется из других:

ts
const double = count.map((n) => n * 2); // =count*2, всегда актуально

Событие (в коде — Stream) — то, что случается, а не есть. Клик, нажатие клавиши, ответ сервера. У клика нет «текущего значения» — он либо произошёл, либо нет. Для новичка достаточно обычных колбэков (onClick={() => …}); события пригодятся позже, для потоков вроде debounce-поиска.

Чем Stream отличается от State — совсем на пальцах

Сравните две вещи из обычной жизни:

  • Температура за окном. Она есть всегда. В любую секунду можно посмотреть на градусник и получить ответ. Вопрос «какая сейчас температура?» всегда имеет смысл.
  • Стук в дверь. Он случается. Между стуками никакого стука нет — вопрос «какой сейчас стук?» бессмысленный. Зато осмысленны вопросы «постучали?» и «что делать, когда постучат?».

Температура — это State. Стук — это Stream. Вся разница — в том, какой вопрос вы задаёте:

ВопросТипПримеры
«Какое оно сейчасStateтекст в поле, счётчик, выбранная вкладка, «залогинен ли», ширина окна
«Произошло ли? Что делать, когда произойдёт?»Streamклик, нажатие Enter, пришёл ответ сервера, тикнул таймер

Проверка одной фразой: если это можно нарисовать на экране — это State. Если на это можно среагировать — это Stream. Число на кнопке рисуют — State. Сам щелчок не нарисуешь — на него реагируют — Stream.

Почему не обойтись одним типом? Попробуйте засунуть клик в «ячейку»: какое значение в ней между кликами? Придётся выдумывать флажок clicked = true и не забывать сбрасывать его обратно — классический источник багов «кнопка сработала дважды». А попробуйте наоборот сделать температуру событием — и чтобы узнать «сколько сейчас», придётся вспоминать последнее уведомление, а если его ещё не было — у вас вообще нет ответа. Каждая вещь неудобна в чужой шкуре — поэтому типа два.

Они дружат и превращаются друг в друга: «последний пришедший ответ сервера» — это уже значение (responses.hold(null): событие → State), а «моменты, когда счётчик изменился» — это уже событие (count.updates: State → событие).

При чём тут «функциональное»

Формулы в Excel — чистые: =A1+B1 не может тайком запустить телевизор или подменить соседнюю ячейку. Именно поэтому таблице можно верить.

«Функциональное» в FRP — то же обещание: производные значения описываются чистыми функциями (map, как формула), и повлиять на значение можно только через объявленные связи. Никаких «подкрутить сбоку». Отсюда главная гарантия: страница не может показать состояние, которого не было.

В Excel это тоже видно: меняя A1, вы никогда не увидите таблицу, пересчитанную «наполовину» — где C1 уже новая, а зависящая от неё D4 ещё старая. Continuum даёт ту же гарантию для интерфейса: любое изменение применяется целиком, одним неделимым шагом. (Внутри это называется «транзакция», но пользоваться этим словом не обязательно — оно просто означает «как в Excel».)

Последний кусочек: компонент — это сборка, а не рисунок

В React компонент — художник: состояние поменялось — перерисовал картину заново. В Continuum компонент — сборщик: он один раз собирает кусок страницы из ячеек и связей — и уходит. Дальше конструкция живёт сама: значение изменилось → обновился ровно тот кусочек DOM, который к нему привязан (мы называем это точечными обновлениями DOM).

tsx
function Counter() {
  const count = state(0); // ячейка
  return (
    <button onClick={() => count.update((n) => n + 1)}>
      count: {count} {/* этот текст привязан к ячейке навсегда */}
    </button>
  );
}

{count} в JSX — не «вставить текущее число», а «этот текст теперь показывает count. Всегда». Функция Counter больше не выполнится ни разу — ей незачем.

Шпаргалка соответствий

ExcelContinuumПо-русски
ячейка со значениемstate(0)реактивное значение
формула =A1*2count.map(n => n * 2)производное значение
формула от двух ячеекcombine(a, b, f)производное от нескольких
пересчёт всей таблицы разомтранзакцияизменение применяется целиком
— (в Excel нет)Streamпоток происшествий: клики, ответы

Что дальше


Незнакомый термин? Вся терминология этой документации объяснена в глоссарии.