Файберы: один примитив вместо трёх подсистем
открытый урокЭтот раздел читается без входа. Войди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти
Файберы: один примитив вместо трёх подсистем
В большинстве языков генераторы, исключения и асинхронность это три независимые подсистемы, каждая со своим синтаксисом. В 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 · Событийный цикл, каналы и таймауты поверх них появляется планировщик: событийный цикл, каналы, таймауты и отмена.
домашка