Раздел 46 · Effect: Тихая Академия

Ссылки со значением по умолчанию

middle~20 мин

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

Сервис из урока 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.

Дальше

Рыдкулл листает тесты башни и считает строки.

Тестовые слои и ленивые сервисы ждут в следующем уроке.