Ночь в подвале. Муравей бегает по трубкам, колокольчик над проводом звенит после каждого кадра, Сундук стоит у лестницы. У входа Пондер прикрутил к стене рычаг. Пока он ни к чему не подключён.
За пять этапов Гекс научился многому: свиток на потом, ошибка в типе, Сундук с сервисами, цикл со стопкой продолжений и колокольчиком, рецепт на генераторе. Свиток он доводит до конца, успехом или провалом. Остановить его снаружи и прибраться за ним он не умеет.
Что нужно Гексу
Проверяется всё, что Гекс умеет с первого этапа: тесты накопительные. Сверху три новые вещи.
- Муравей, которого можно остановить.
IO.runForkзапускает свиток и сразу отдаёт файбер, муравья с двумя ручками:await()дождатьсяExit,interrupt()дёрнуть рычаг. Прерывание видно перед следующей инструкцией, а если муравей ждёт колокольчика, Гекс отменяет ожидание отменялкой из прошлого урока. УExitпоявляется третья причина провала,Interrupt, иcatchеё не ловит, как не ловит дефект: рычаг не ошибка свитка, а решение снаружи. - Уборка при любом исходе.
IO.ensuring(self, finalizer)исполняет финализатор послеself, кончился тот успехом, провалом или прерыванием. Вложенные финализаторы идут изнутри наружу. Финализатор по типу не падает, у негоE = never, и рычаг его не прерывает: уборку доводят до конца. - Ресурс.
IO.acquireUseRelease(acquire, use, release)берёт ресурс, работает с ним и отпускает при любом исходе работы. Пока ресурс берут, рычаг ждёт: иначе ставни могли бы открыться, а закрыть их было бы некому.
И ночная передача в башне, relayAtNight: ставни это ресурс, кадры уходят по одному с паузой, ставни закрываются всегда.
Сигнатуры, порядок финализаторов и сценарии, на которых ловят тесты, описаны в заказе до символа.
Чек-лист перед сдачей
- Тесты всех прошлых этапов зелёные: сто тысяч
flatMap, колокольчик,genна ста тысячах шагов. - Рычаг, дёрнутый дважды, зовёт отменялку ожидания один раз. Ответ колокольчика, пришедший после рычага, никуда не идёт.
catchвокруг прерванного свитка не превращает прерывание в успех.- У
ensuring(ensuring(body, a), b)при любом исходе сначалаa, потомb, и каждый ровно один раз. - Финализатор со
sleepвнутри доводится до конца, даже если рычаг дёрнули до него. - Рычаг во время медленного
acquire: ресурс берётся,useне начинается,releaseисполняется. runSyncExitна ждущем свитке не только отвечает дефектом, но и прерывает свиток: его продолжение после ожидания не исполнится никогда.ensuring(IO.succeed(1), IO.fail('x'))не собирается. Проверь это под@ts-expect-error, а не на глаз.- Сломай Гекс нарочно, например проверяй рычаг только у колокольчика, и убедись, что тест про рассвет покраснел. Потом верни.
Гекс, этап 6
Последний заказ раздела: Гекс, который умеет остановиться и прибраться. После него свиток можно запустить на ночь и быть уверенным, что на рассвете ставни будут закрыты.
Ринцвальд
Пока ты пишешь, Ринцвальд кладёт на стол свою ночную передачу. Он считает, что Гекс для этого не нужен.
const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
const log: string[] = [];
const relayAtNight = async (frames: readonly string[]): Promise<number> => {
log.push('открыты');
try {
for (const frame of frames) {
log.push(frame);
await sleep(20);
}
return frames.length;
} finally {
log.push('закрыты');
}
};
const main = async () => {
const dawn = sleep(30).then(() => 'рассвет');
console.log(await Promise.race([relayAtNight(['a', 'b', 'c']), dawn]));
console.log(log);
await sleep(80);
console.log(log);
};
void main();
рассвет
[ 'открыты', 'a', 'b' ]
[ 'открыты', 'a', 'b', 'c', 'закрыты' ]
Рассвет победил, и main пошла дальше. А передача нет: Promise.race выбирает, чей ответ взять, но никого не останавливает. После рассвета ушёл кадр c, и ставни закрылись, когда передача сама кончилась. finally честно сработал, только поздно. Промис уже летит, и отменить его нечем: у него нет рычага, а AbortController пришлось бы протянуть через каждый await руками, как в уроке 02.
Практика
Итог
К рассвету Рыдкулл спускается в подвал, ставит передачу на ночь и в шесть утра дёргает рычаг. Муравей останавливается посреди паузы между кадрами. Наверху щёлкают ставни: закрыты.
Пондер достаёт из шкафа остроконечную шляпу, мятую, с вышитым словом «ВАЛШЕБНИК». Так её носят бакалавры: с ошибкой, чтобы помнили, с чего начинали. Ринцвальд смотрит на шляпу, потом на свою.
Ранг бакалавр за этот раздел, и шляпа с ним: её можно надеть в гардеробе.
А теперь настоящий Effect
Свой Гекс уместился в один модуль, и каждая его часть есть в Effect. Вот соответствия и то, где наш Гекс упрощает:
| Свой Гекс | Effect 4 | Что у Effect иначе |
|---|---|---|
IO.IO<A, E, R> | Effect.Effect<A, E, R> | в R ключи-классы Context.Service, а не строковые имена |
IO.succeed, IO.sync, IO.fail | Effect.succeed, Effect.sync, Effect.fail | то же |
IO.catch, IO.mapError | Effect.catch, Effect.mapError | у нас только data-first, у Effect два вида вызова и pipe |
IO.service(Wire), IO.provideService | yield* Wire или Effect.service(Wire), Effect.provideService | ключ сервиса сам идёт в yield* |
IO.callback, IO.sleep | Effect.callback, Effect.sleep | register получает ещё AbortSignal и возвращает не отменялку, а эффект очистки |
IO.gen | Effect.gen | то же |
Exit, Cause: Fail, Die, Interrupt | Exit, Cause | у Cause массив reasons: причин бывает несколько сразу, например провал и упавший финализатор |
IO.ensuring, IO.acquireUseRelease | Effect.ensuring, Effect.acquireUseRelease | release получает ещё Exit работы; есть Scope, Effect.acquireRelease и Effect.scoped, это EF6 |
IO.runSync, IO.runSyncExit, IO.runPromise, IO.runPromiseExit | Effect.runSync, Effect.runSyncExit, Effect.runPromise, Effect.runPromiseExit | то же |
IO.runFork, fiber.await(), fiber.interrupt() | Effect.runFork, Fiber.await, Fiber.join, Fiber.interrupt | у Effect это эффекты, их запускают, а не методы с промисом |
| один муравей | много файберов и планировщик | муравей уступает трубку другим, а fork запускает муравья изнутри свитка; это EF7 |
Та же ночная передача на Effect 4, с рассветом через полминуты:
import { Context, Data, Effect, Exit, Fiber } from 'effect';
class WireDown extends Data.TaggedError('WireDown')<{ readonly tower: string }> {}
class Wire extends Context.Service<
Wire,
{ readonly send: (frame: string) => Effect.Effect<void, WireDown> }
>()('Wire') {}
class Shutters extends Context.Service<
Shutters,
{ readonly open: Effect.Effect<void>; readonly close: Effect.Effect<void> }
>()('Shutters') {}
const relayAtNight = (frames: readonly string[]) =>
Effect.gen(function* () {
const shutters = yield* Shutters;
return yield* Effect.acquireUseRelease(
shutters.open,
() =>
Effect.gen(function* () {
const wire = yield* Wire;
for (const frame of frames) {
yield* wire.send(frame);
yield* Effect.sleep('20 millis');
}
return frames.length;
}),
() => shutters.close,
);
});
const log: string[] = [];
const night = relayAtNight(['a', 'b', 'c']).pipe(
Effect.provideService(Wire, { send: (frame) => Effect.sync(() => void log.push(frame)) }),
Effect.provideService(Shutters, {
open: Effect.sync(() => void log.push('открыты')),
close: Effect.sync(() => void log.push('закрыты')),
}),
);
const main = async () => {
const fiber = Effect.runFork(night);
await new Promise((resolve) => setTimeout(resolve, 30));
await Effect.runPromise(Fiber.interrupt(fiber));
const exit = await Effect.runPromise(Fiber.await(fiber));
console.log(Exit.hasInterrupts(exit), log);
};
void main();
true [ 'открыты', 'a', 'b', 'закрыты' ]
Свиток читается почти строка в строку как твой relayAtNight. Кадр c не ушёл, ставни закрыты, Exit кончился прерыванием. Разница в том, что лежит под ним, и её ты теперь знаешь изнутри.
Глубже в архиве
acquireUseRelease это старый приём bracket из Haskell: взять, использовать, отпустить, даже если посередине исключение или асинхронная отмена. Как он устроен там и почему отпускание тоже защищено от прерываний, разбирает урок «Исключения и ресурсы».
Дальше
Утром Рыдкулл приходит в подвал не один. Под мышкой у него распечатка, которую ночью вынесли из конторы Магистрали.
declare const relay: (frame: string) => Promise<void>;
export const send = async (frame: string): Promise<string> => {
try {
await relay(frame);
} catch {}
return 'успешно';
};
Пустой catch и «успешно» после него, что бы ни случилось с кадром. Ошибки на Магистрали не исчезали: их прятали. Гекс своё дело сделал, ты знаешь, как устроен рантайм. Дальше настоящий Effect и настоящая башня.
Следующий раздел курса это EF4 «Первые чары». Рыдкулл берёт тебя в работу над башней T-1, и вы переписываете её обработчик кадров на Effect: квитанция Effect<A, E, R>, Effect.gen и pipe, ожидаемые ошибки и дефекты, Cause и Exit. Начинается он с урока 36.