Сервис из урока 47 это долг: пока его не положили, квитанцию не запустить. Для провода и книги это правильно, без них башне делать нечего. Но бывают значения, у которых есть разумный вариант по умолчанию: имя смены, уровень подробности журнала, часы. Требовать их в R значит наказывать всех ради редкого случая, когда значение другое.
Для таких значений в Effect есть ссылки.
Context.Reference
Ссылку объявляют так:
export const ShiftName = Context.Reference<string>('bashnya/ShiftName', {
defaultValue: () => 'ночь',
});
Ключ с путём пакета, как у сервиса. Форма string. И defaultValue: функция, которая даёт значение, если в контексте ссылку никто не положил. Функция, а не значение, затем, чтобы значение считалось лениво, при первом вопросе, а не при загрузке модуля. Считается оно один раз и дальше одно на всю программу, поэтому изменяемое состояние, как журнал из урока 48, в значение по умолчанию не кладут.
Читают ссылку так же, как сервис:
export const namedShift = Effect.fn('Tower.namedShift')(function* (
lines: readonly string[],
): Effect.fn.Return<NightReport, never, Wire | Ledger | Journal> {
const name = yield* ShiftName;
yield* Journal.use((journal) => journal.write(`смена «${name}»`));
return yield* nightShift(lines);
});
Посмотри на третий параметр Effect.fn.Return. В R провод, книга и журнал, а ShiftName нет, хотя функция его читает. Это не забывчивость: у ссылки есть значение по умолчанию, долга нет. Компилятор согласен, тест этапа проверяет этот тип.
Подмена на кусок программы
Положить ссылку можно тем же provideService, что и сервис. И она действует только внутри того, чему её положили:
const name = Effect.gen(function* () {
return yield* ShiftName;
});
console.log(
Effect.runSync(Effect.all([name, Effect.provideService(name, ShiftName, 'утро'), name])),
);
[ 'ночь', 'утро', 'ночь' ]
До подмены и после неё ночь, внутри утро. Это не присваивание глобальной переменной, а новая квитанция с дополненным контекстом: старая квитанция и всё, что рядом с ней, видят прежний контекст.
Перевод с Ук
Для сравнения то же имя смены, только сервисом:
class ShiftName extends Context.Service<ShiftName, string>()('bashnya/ShiftName') {}
const header = Effect.gen(function* () {
return `смена «${yield* ShiftName}»`;
});
console.log(Effect.runSync(header));
ex52d.ts(9,28): error TS2379: Argument of type 'Effect<string, never, ShiftName>' is not assignable to parameter of type 'Effect<string, never, never>' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties.
Type 'ShiftName' is not assignable to type 'never'.
Знакомая с урока 47 последняя строка: в R долг ShiftName, а runSync долгов не принимает. Вопросительное «Ук» Библиотекаря значит «а ты точно хотел долг?». С Context.Reference долга нет и ошибки нет. Выбор между сервисом и ссылкой это вопрос «есть ли у значения честный вариант по умолчанию». У провода нет: провод по умолчанию, который никуда не шлёт, спрячет ошибку сборки. У имени смены есть.
Ссылки самого Effect
Effect сам держит настройки рантайма ссылками, в модуле References. Самые ходовые про журнал Effect, то есть про Effect.log:
References.CurrentLogLevelуровень, с которым пишетсяEffect.logбез явного уровня;References.MinimumLogLevelпорог: всё, что ниже, отбрасывается. По умолчаниюInfo.
const night = Effect.gen(function* () {
yield* Effect.logDebug('кадр 5 разобран');
yield* Effect.logInfo('ночь закрыта');
});
Effect.runSync(night);
console.log('---');
Effect.runSync(Effect.provideService(night, References.MinimumLogLevel, 'Debug'));
[19:16:41.466] INFO (#1): ночь закрыта
---
[19:16:41.468] DEBUG (#2): кадр 5 разобран
[19:16:41.468] INFO (#2): ночь закрыта
Отладка одной подозрительной ночи без пересборки и без переменной окружения: подробный журнал включён ровно для одного куска программы, соседние пишут как обычно.
В Effect 3 такие значения жили в FiberRef, отдельном механизме со своими функциями. В Effect 4 FiberRef убран: ссылка это сервис с умолчанием, и работает с ним тот же provideService.
Под капотом
yield* ShiftName смотрит в контекст по ключу 'bashnya/ShiftName'. Нашёл, отдаёт найденное. Не нашёл, зовёт defaultValue и запоминает ответ навсегда: следующий вопрос, даже из другого запуска, получит то же самое. Для компилятора разница в одном месте: тип ссылки помечен так, что Effect.gen не добавляет её в R. Больше разницы нет, поэтому provideService одинаково кладёт и сервис, и ссылку.
Где тут дыра
let shiftName = 'ночь';
const header = (name: string) =>
Effect.gen(function* () {
shiftName = name;
yield* Effect.sleep('10 millis');
return `смена «${shiftName}»`;
});
console.log(await Effect.runPromise(Effect.all([header('ночь'), header('утро')], { concurrency: 2 })));
[ 'смена «утро»', 'смена «утро»' ]
По одной смене за раз всё работает. Запусти две смены одновременно (concurrency: 2 значит «обе сразу», подробно в EF7), и пока ночная ждёт, утренняя перезаписывает переменную. Ночная смена подписывается утренней. Это та же глобальная проводка из урока 47: одно значение на всю программу, хотя смен две.
Ссылка эту дыру закрывает по устройству: Effect.provideService(смена, ShiftName, 'утро') меняет контекст только утренней квитанции, и ночная его не видит, даже если смены идут вперемешку.
Башня, этап 17
Смена получает имя. По умолчанию это «ночь», и никто не обязан его класть. Тот, кому нужна утренняя смена, подменяет имя для своего куска программы.
Практика
Глубже в архиве
Ссылки с умолчанием похожи на окружение монады Reader и её local: «Reader, Writer, State» в разделе Haskell показывает ту же мысль без Effect.
Дальше
Рыдкулл листает тесты башни и считает строки.
Тестовые слои и ленивые сервисы ждут в следующем уроке.