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

Граф слоёв

middle~25 мин

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

Три provide подряд из прошлого урока работают, но у них два недостатка. Порядок важен и нигде не записан, кроме головы того, кто писал. И сборка размазана по месту запуска: тест на ночь и запуск ночи повторяют одни и те же три строки.

Слои можно собирать друг из друга ещё до того, как их отдали квитанции. Получится один слой, «башня целиком», с понятным типом. Его кладут одним provide.

Граф зависимостей

Нарисуй, кто кому нужен:

смена  ──▶ Wire ──▶ Journal
   │                  ▲
   ├──▶ Ledger        │
   └──────────────────┘

Смене нужны все три сервиса. Проводу нужен журнал. Книге не нужно ничего. Стрелки не образуют круга, и это главное: такой граф можно собрать снизу вверх. Сначала журнал, потом провод, которому журнал отдают, книгу в любой момент. Это граф слоёв.

Три способа сложить слои

Layer.merge и Layer.mergeAll кладут слои рядом. Каждый собирается сам по себе, результат даёт всё, что дают они, и просит всё, что просят они:

Layer.mergeAll(Wire.layerTest, Ledger.layer([1, 2]))
// Layer<Wire | Ledger, never, Journal>

mergeAll ничего не передаёт от одного слоя другому. Провод просит журнал, и объединение просит журнал.

Layer.provide кормит один слой другим. a.pipe(Layer.provide(b)) значит: собери b, отдай его a, а наружу выдай только a:

Layer.mergeAll(Wire.layerTest, Ledger.layer([1, 2])).pipe(Layer.provide(Journal.layer))
// Layer<Wire | Ledger, never, never>

Журнал ушёл из требований, проводу его дали. Но и в том, что слой даёт, журнала нет: Layer.provide прячет то, чем кормил.

Layer.provideMerge кормит и оставляет на виду. Собери b, отдай его a, а наружу выдай и a, и b:

export const testLayer = (sent: Iterable<number>) =>
  Layer.mergeAll(Wire.layerTest, Ledger.layer(sent)).pipe(
    Layer.provideMerge(Journal.layer),
    Layer.satisfiesServicesType<never>(),
  );
// Layer<Wire | Ledger | Journal, never, never>

Это и есть башня для тестов: даёт три сервиса, ничего не просит. Журнал в ней один: провод получил тот же экземпляр, что выдан наружу, и смена, которая возьмёт журнал из контекста, возьмёт его же.

Что делает последняя строка, satisfiesServicesType, разберём ниже, после ошибки, которую она ловит.

Перевод с Ук

Возьми Layer.provide вместо provideMerge и отдай слой смене:

const hidden = Layer.mergeAll(Wire.layerTest, Ledger.layer([1, 2])).pipe(
  Layer.provide(Journal.layer),
);

const report = Effect.runPromise(nightShift(['T-5->T-1|10|Ping|']).pipe(Effect.provide(hidden)));
ex49a.ts(10,34): error TS2379: Argument of type 'Effect<NightReport, never, Journal>' is not assignable to parameter of type 'Effect<NightReport, never, never>' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties.
  Type 'Journal' is not assignable to type 'never'.

Последняя строка, как всегда: в R остался Journal. Слой hidden дал проводу журнал, но спрятал его: наружу вышли только Wire | Ledger. Смене журнал тоже нужен, сводку она пишет сама, и взять его неоткуда. Ошибка не в слое, со слоем всё в порядке. Ошибка в том, что смене нужен сервис, который слой держит внутри.

provide годится, когда внутренний сервис нужен только слою: например, соединение с базой, которое никто, кроме хранилища, не трогает. Когда сервис нужен и слою, и программе, нужен provideMerge.

Проверка на месте сборки

Теперь другая ошибка. Забудь provideMerge совсем и сложи три слоя рядом:

export const testLayer = (sent: Iterable<number>) =>
  Layer.mergeAll(Wire.layerTest, Ledger.layer(sent), Journal.layer).pipe(
    Layer.satisfiesServicesType<never>(),
  );
ex49b.ts(7,5): error TS2345: Argument of type '<ROut, E, RIn2 extends never>(layer: Layer<ROut, E, RIn2>) => Layer<ROut, E, RIn2>' is not assignable to parameter of type '(_: Layer<Wire | Journal | Ledger, never, Journal>) => Layer<Wire | Journal | Ledger, never, never>'.
  Types of parameters 'layer' and '_' are incompatible.
    Type 'Layer<Wire | Journal | Ledger, never, Journal>' is not assignable to type 'Layer<Wire | Journal | Ledger, never, never>' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties.
      Type 'Journal' is not assignable to type 'never'.

Журнал в слое есть, но mergeAll не отдаёт его проводу: соседи друг друга не кормят. Объединение даёт журнал и тут же просит его для провода. Без последней строки это скомпилировалось бы, и ошибка вылезла бы далеко отсюда, в runPromise у того, кто положит слой.

Layer.satisfiesServicesType<never>() это проверка без действия: она возвращает тот же слой, но требует, чтобы его RIn подходил под never. Ошибка встаёт на месте сборки, строка 7, с той же последней строкой, что и всегда: Journal не подходит к never. Ставь её в конец каждой сборки, которая должна быть самодостаточной.

Где провод настоящий

Для ночи на Магистрали нужен провод, который отправляет по-настоящему и пишет в журнал только то, что провод принял:

// внутри class Wire
static readonly layer = (send: (line: string) => Effect.Effect<void, SendFailed>) =>
  Layer.effect(
    Wire,
    Effect.gen(function* () {
      const journal = yield* Journal;
      return Wire.of({ send: (line) => Effect.tap(send(line), () => journal.write(`> ${line}`)) });
    }),
  );

Effect.tap пишет в журнал только после успеха send: оборванная строка в журнал не попадёт, а SendFailed пойдёт дальше. Башня поверх старого объекта собирается так же, как тестовая, только с настоящим проводом:

export const towerLayer = (tower: Tower) =>
  Layer.mergeAll(Wire.layer((line) => sendLine(tower.send, line)), Ledger.layer(tower.sent)).pipe(
    Layer.provideMerge(Journal.layer),
    Layer.satisfiesServicesType<never>(),
  );

export const runShift = (tower: Tower, lines: readonly string[]): Promise<readonly string[]> =>
  Effect.runPromise(
    nightShift(lines).pipe(
      Effect.andThen(Journal.use((journal) => journal.entries)),
      Effect.provide(towerLayer(tower)),
    ),
  );
const night = ['T-3->T-1|5|Text|%D0%B3', 'T-9->T-1|7|Ping|', 'T-5->T-1|10|Ping|'];
const tower = {
  send: async (line: string) => {
    if (line.includes('T-9')) throw new Error('обрыв');
  },
  sent: new Set([1, 2]),
};
console.log(await runShift(tower, night));
[
  '> T-1->T-3|5|Ack|5',
  '> T-1->T-5|10|Ping|',
  'обработано 2, ошибок 1, призраков 0, дублей 0, сбоев 0'
]

Ответ на T-9 оборвался: в журнале его нет, в сводке он ошибка. С testLayer([1, 2]) та же ночь даёт три строки > и ни одной ошибки: дублёр не рвётся.

Сколько журналов вышло

Граф собран, журнал один. Посмотри, как одна и та же башня собирается по-разному и сколько журналов получается. Первые две сборки ты уже знаешь; остальные, где журнал расходится, понадобятся в следующем уроке.

Где тут дыра

const testLayer = Layer.mergeAll(Wire.layerTest, Ledger.layer([1, 2]), Journal.layer) as Layer.Layer<
  Wire | Ledger | Journal
>;

const exit = await Effect.runPromiseExit(
  nightShift(['T-5->T-1|10|Ping|']).pipe(Effect.provide(testLayer)),
);
if (exit._tag === 'Failure')
  console.log(exit.cause.reasons.map((reason) => (reason._tag === 'Die' ? String(reason.defect) : reason._tag)));
[ 'Error: Service not found: bashnya/Journal' ]

as заставил компилятор замолчать, но не дал проводу журнал. При сборке Wire.layerTest делает yield* Journal, смотрит в словарь по ключу 'bashnya/Journal' и ничего не находит: соседний Journal.layer в mergeAll проводу ничего не передаёт. Падение это дефект, Die, при первой же ночи, а не ошибка компилятора за минуту до коммита.

Лечится тем, что as уходит, а на его место встаёт Layer.provideMerge(Journal.layer). Компилятор ругался по делу. R и RIn это единственное место, где граф проверяют до запуска; приведение типа его отключает.

Башня, этап 14

Башня собирается одной вещью. Настоящий провод пишет в журнал только принятое, towerLayer собирает башню поверх старого объекта, testLayer собирает её для тестов, обе с пустыми требованиями. runShift на краю прогоняет ночь и отдаёт журнал.

Практика

Дальше

Ринцвальд дождался, пока Рыдкулл уйдёт, и показывает свой обработчик.

Утром журнала ночи у Ринцвальда нет: каждый кадр завёл свой журнал и унёс его с собой. Виджет выше уже подсказывает, как так вышло. Сколько раз Effect собирает слой и когда собирает его заново, разберём в следующем уроке.