Раздел 31 · Janet на практике

Файберы: один примитив вместо трёх подсистем

middle-senior~25 мин

открытый урокЭтот раздел читается без входа. Войди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Файберы: один примитив вместо трёх подсистем

В большинстве языков генераторы, исключения и асинхронность это три независимые подсистемы, каждая со своим синтаксисом. В Janet это одна вещь, и в этом уроке мы посмотрим, как из неё вырастают все три.

Идея

Файбер это вычисление, которое можно приостановить и потом продолжить. Картинка для головы: закладка в книге. Ты откладываешь книгу, и она ждёт с сохранённой страницей и пометками на полях, это стек и локальные переменные. Взял снова, читаешь со следующего слова, а не с начала главы.

Приостановка называется сигналом, и получает его тот, кто файбер запустил. Дальше всё сводится к тому, какие сигналы файбер шлёт и кто их ловит:

  • сигнал “вот очередное значение” даёт генератор;
  • сигнал “произошла ошибка” даёт обработку исключений;
  • сигнал “жду ввод-вывод” даёт асинхронность.

Если тебе знакомы генераторы JavaScript, механика будет узнаваемой, мы разбирали её в уроке про корутины. Разница в том, что в JS function* умеет только первое из трёх, а в Janet тот же примитив несёт все три роли.

Базовая механика

Файбер создаётся из функции без аргументов и не запускается сразу:

resume запускает или продолжает файбер. Он возвращает то, что файбер отдал через yield, либо, если файбер дошёл до конца, его возвращаемое значение:

Продолжить мёртвый файбер нельзя:

(fiber/can-resume? f)   # false

Статусы, которые встречаются на практике:

СтатусКогда его увидишь
:newфайбер создан, resume ещё не было
:pendingзамер на yield, ждёт следующего resume
:aliveвыполняется прямо сейчас; снаружи не застать, виден только изнутри самого файбера
:deadтело дошло до конца, продолжить нельзя
:errorупал с ошибкой, разберём ниже вместе с маской :e

yield это не return

yield отдаёт значение наружу, а не возвращает. Функция не завершается, она замирает ровно на этом месте, а её локальные переменные и стек сохраняются. Следующий resume продолжит со следующего выражения.

Пройди все три сценария по шагам, слева видно, на какой строке замер файбер:

Передача значений внутрь

Общение двустороннее. resume может передать значение обратно в файбер, и оно станет результатом того самого yield, на котором файбер стоял:

Это превращает файбер из генератора в полноценный канал общения: можно не только получать значения, но и отвечать на них.

Генераторы

Файбер можно перебирать напрямую, each понимает файберы:

Для типичного случая есть макрос generate с синтаксисом обычного цикла:

Главное свойство это лень. Значения вычисляются по одному, по мере запроса:

Поэтому генератор может быть бесконечным: пока ты не просишь следующий элемент, работа не делается.

Ещё короче это макрос coro, оборачивающий тело в файбер:

Правило выбора между ними: generate берёт заголовок цикла и даёт ленивую последовательность, coro заворачивает произвольное тело, где yield расставлен руками.

Ошибки это тоже сигнал

Ошибка в Janet не отдельный механизм, а сигнал файбера. По умолчанию файбер его не перехватывает, и ошибка летит наружу к вызывающему:

Но файбер можно создать с маской сигналов, списком того, что он ловит сам. Маска :e означает “перехватывать ошибки”:

Именно так реализован try: он оборачивает тело в файбер с маской :e и смотрит на статус. Никакой отдельной подсистемы исключений в языке нет, есть только файберы.

В повседневном коде файберы для ошибок руками писать не нужно, есть готовые обёртки:

(try (risky) ([err] (handle-err err)))
(protect (risky))   # (true значение) или (false ошибка)

protect берут, когда ошибку надо передать дальше как данные, а не обработать на месте.

Осталась третья роль из списка в начале урока. Сигнал “жду ввод-вывод” обслуживает уже не твой код, а планировщик, и это тема урока 31-janet/13 · Событийный цикл, каналы и таймауты.

Что запомнить

  • Файбер это приостанавливаемое вычисление, приостановка это сигнал.
  • Из одного примитива растут генераторы, исключения и асинхронность.
  • fiber/new создаёт, resume запускает и продолжает, yield отдаёт значение и замирает.
  • yield это не return: стек и локальные переменные сохраняются.
  • resume со значением передаёт его внутрь, оно становится результатом yield.
  • generate и coro это готовые обёртки, each понимает файберы напрямую.
  • Генератор ленив, поэтому может быть бесконечным.
  • Маска :e заставляет файбер ловить свои ошибки. На этом построен try.

Упражнения

Дальше

Пока мы переключали файберы руками. В 31-janet/13 · Событийный цикл, каналы и таймауты поверх них появляется планировщик: событийный цикл, каналы, таймауты и отмена.

домашка

Домашка