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

Сколько раз построен слой

middle-senior~20 мин

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

В прошлом уроке башня стала одной вещью, testLayer, и журнал в ней один. Но «один» в графе ещё не значит «один» в программе. Слой это фабрика, а фабрику можно позвать сколько угодно раз. Этот урок о том, когда Effect зовёт фабрику заново, а когда отдаёт уже собранное.

Башня на каждый кадр

Вот обработчик Ринцвальда:

const frames = ['T-3->T-1|5|Text|%D0%B3', 'мусор', 'T-5->T-1|10|Ping|'];

const journals = await Effect.runPromise(
  Effect.forEach(frames, (line) =>
    Effect.andThen(nightShift([line]), Journal.use((journal) => journal.entries)).pipe(
      Effect.provide(testLayer([1, 2])),
    ),
  ),
);
console.log(journals);
[
  [
    '> T-1->T-3|5|Ack|5',
    'обработано 1, ошибок 0, призраков 0, дублей 0, сбоев 0'
  ],
  [ 'обработано 0, ошибок 1, призраков 0, дублей 0, сбоев 0' ],
  [
    '> T-1->T-5|10|Ping|',
    'обработано 1, ошибок 0, призраков 0, дублей 0, сбоев 0'
  ]
]

Три кадра, три башни, три журнала по одной сводке. Ни один не помнит ночь целиком. И это не всё: если тот же кадр придёт дважды, каждая башня увидит его впервые, и дубль не поймает никто. Отчёт о ночи из трёх отчётов о кадре не склеить.

Ринцвальд хотел изоляции: один кадр сломался, остальным всё равно. Изоляцию даёт Effect.exit в processNight из урока 46, а не новая башня. Башня на кадр это цена без пользы.

Правило Effect 4

Сколько раз Effect собирает слой, решает одно правило с тремя частями.

  1. Внешний provide переиспользуют. Если слой уже собран снаружи, а программа внутри снова отдаёт его через provide, Effect возьмёт собранный. Второго журнала не будет.
  2. Соседние provide собирают своё. Два provide, которые не вложены друг в друга, ничего не знают друг о друге. У Ринцвальда forEach зовёт provide трижды, рядом, и каждый собирает башню заново.
  3. Узнают слой по ссылке. «Тот же слой» значит тот же объект. Wire.layerTest это одно статическое поле, его узнают всегда. Ledger.layer([1, 2]) это функция, и каждый вызов делает новый объект.

Третью часть проще всего проверить на счётчике:

let builds = 0;
class Book extends Context.Service<Book, number>()('Book') {
  static readonly layer = (n: number) => Layer.effect(Book, Effect.sync(() => (builds++, n)));
}
const read = Book.useSync((n) => n);

const one = Book.layer(1);
Effect.runSync(Effect.all([read, read.pipe(Effect.provide(one))]).pipe(Effect.provide(one)));
// builds: 1

Effect.runSync(
  Effect.all([read, read.pipe(Effect.provide(Book.layer(1)))]).pipe(Effect.provide(Book.layer(1))),
);
// builds: 2

В первом случае внутренний provide получил тот же объект one, что и внешний, и Effect отдал собранное. Во втором Book.layer(1) вызван дважды: два объекта, две сборки, хоть параметры и одинаковые. Это мемоизация по ссылке, и из неё следует практический совет: слой, который должен быть общим, держи в одной константе или в статическом поле, а не вызывай функцию в каждом месте.

Когда свой журнал нужен

Теперь обратная задача. Академия хочет день из нескольких смен: книга отправленных одна на весь день, а журнал у каждой смены свой, чтобы утром на стол легла отдельная тетрадь. День собран на testLayer, а каждая смена кладёт себе свои провод и журнал:

const entries = Journal.use((journal) => journal.entries);
const shiftLayer = Wire.layerTest.pipe(Layer.provideMerge(Journal.layer));

const program = (local: boolean) =>
  Effect.gen(function* () {
    yield* Journal.use((journal) => journal.write('день начат'));
    const inner = yield* Effect.andThen(nightShift(['T-5->T-1|10|Ping|']), entries).pipe(
      Effect.provide(shiftLayer, { local }),
    );
    return { inner, outer: yield* entries };
  }).pipe(Effect.provide(testLayer([1, 2])));

Без local:

{
  inner: [
    'день начат',
    '> T-1->T-5|10|Ping|',
    'обработано 1, ошибок 0, призраков 0, дублей 0, сбоев 0'
  ],
  outer: [
    'день начат',
    '> T-1->T-5|10|Ping|',
    'обработано 1, ошибок 0, призраков 0, дублей 0, сбоев 0'
  ]
}

Смена хотела свою тетрадь, а получила дневную: правило 1 сработало ровно так, как написано. shiftLayer новый объект, но собран он из Wire.layerTest и Journal.layer, а их внешний testLayer уже собрал. Effect узнал оба по ссылке и отдал готовые. Запись смены легла в дневной журнал, рядом с «день начат».

С { local: true }:

{
  inner: [
    '> T-1->T-5|10|Ping|',
    'обработано 1, ошибок 0, призраков 0, дублей 0, сбоев 0'
  ],
  outer: [ 'день начат' ]
}

Effect.provide(слой, { local: true }) собирает слой целиком заново, со всеми его частями, не заглядывая в то, что собрано снаружи. Свой провод, свой журнал, а книга, которую shiftLayer не даёт, приходит из дня, общая.

Есть и второй способ, точечный: Layer.fresh(Journal.layer). Это обёртка, которую не узнают по ссылке, поэтому собирается заново именно она, а всё остальное по общим правилам. local говорит «эту сборку целиком с нуля», fresh говорит «вот этот слой всегда с нуля».

Посмотри все шесть сборок в одном месте. Последние три это ровно то, что разобрано выше: башня на кадр, вложенная смена без local и с ним.

Под капотом

Когда provide собирает слой, он заводит таблицу MemoMap: ключ это объект слоя, значение это собранный кусок контекста. Перед тем как собрать любую часть графа, сборщик смотрит в таблицу. Нашёл, отдаёт найденное. Не нашёл, собирает и записывает.

Внешний provide кладёт свою таблицу в контекст программы, и вложенный provide её находит и пользуется ей же: отсюда правило 1. Соседние provide таблиц друг друга не видят: правило 2. Ключ это объект, а не его содержимое: правило 3. { local: true } заводит новую пустую таблицу для одной сборки. Layer.fresh возвращает новый объект-обёртку, которого в таблице нет и не будет, потому что ссылку на него при каждой сборке берут заново.

Где тут дыра

Дыра этого урока уже разобрана, это обработчик Ринцвальда в начале: Effect.provide(testLayer(...)) внутри функции, которую зовут на каждый кадр. Она коварна тем, что в тесте на один кадр всё работает и выглядит даже аккуратнее. Ищи её по признаку: provide стоит не на краю программы, а внутри функции, которую зовут много раз. Если такая функция нужна, слой отдают ей снаружи, а не собирают внутри.

Башня, этап 15

Башня учится работать днём из нескольких смен. ownShift даёт куску программы свои провод и журнал, даже если снаружи уже собраны общие. day прогоняет несколько ночей подряд на одной книге и отдаёт журнал каждой смены отдельно.

Практика

Дальше

Рыдкулл стоит у окна и смотрит на холм за рекой. Там поднимают леса.

Конфигурация и секреты ждут в следующем уроке.