Маршрут из урока 77 это Tuple из двух строк, а из урока 80 пара с необязательным ретранслятором. Для цепочки любой длины ни кортеж, ни массив не годятся по смыслу: у перегона есть башня и, может быть, следующий перегон, который устроен так же. Схема должна ссылаться сама на себя. А вторая просьба Казимира вообще не про форму: чтобы узнать, есть ли башня, надо спросить книгу башен, то есть мир.
Цепочка
Сначала тип. Перегон это башня и, если кадр идёт дальше, следующий перегон:
export interface Hop {
readonly tower: TowerCode;
readonly next?: Hop;
}
Тип на себя сослаться может: TypeScript разрешает интерфейсу упоминать себя в полях. Теперь схема. Первая попытка в лоб:
export const Hop = Schema.Struct({
tower: Schema.String,
next: Schema.optionalKey(Hop),
});
Это не соберётся и не запустится. Hop справа ещё не объявлен: значение const читают раньше, чем оно появилось. С типами так можно, со значениями нельзя. Нужна отсрочка: не сама схема, а функция, которая отдаст её потом. Это Schema.suspend:
export const Hop = Schema.Struct({
tower: TowerCode,
next: Schema.optionalKey(Schema.suspend(() => Hop)),
});
Перевод с Ук
hop.ts(5,14): error TS7022: 'Hop' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.
hop.ts(7,43): error TS7024: Function implicitly has return type 'any' because it does not have a return type annotation and is referenced directly or indirectly in one of its return expressions.
Читаем снизу вверх. Вторая ошибка про стрелку () => Hop: чтобы узнать, что она возвращает, нужно знать тип Hop. Первая про сам Hop: чтобы узнать его тип, нужно вывести тип всего выражения справа, а внутри опять стрелка. Компилятор выводит типы изнутри наружу, и здесь у него нет начала: вывод ходит по кругу. Тогда он сдаётся и пишет «неявно any», а под strict это ошибка.
Круг разрывают явной подписью. Мы говорим компилятору тип заранее, и выводить ему больше нечего:
export interface HopEncoded {
readonly tower: string;
readonly next?: HopEncoded;
}
export const Hop: Schema.Codec<Hop, HopEncoded> = Schema.Struct({
tower: TowerCode,
next: Schema.optionalKey(Schema.suspend((): Schema.Codec<Hop, HopEncoded> => Hop)),
});
Schema.Codec<Type, Encoded> это схема с двумя сторонами из урока 79. Поэтому интерфейсов два: в башне у перегона TowerCode с брендом, на проводе простая строка. Компилятору хватает подписи у константы: стрелка берёт тип оттуда. В эталоне подпись стоит и у стрелки, чтобы читатель не искал, что она возвращает.
[ 'T-1', 'T-4', 'T-9' ]
код башни вида T-число
at ["next"]["next"]["tower"]
Цепочка из трёх башен разбирается, а битый код в третьем перегоне Schema находит и называет путь целиком. towersOf из задачи этапа выпрямляет цепочку в список.
Под капотом
Схема в Effect это дерево описаний, SchemaAST. Schema.suspend(f) кладёт в это дерево узел, в котором лежит только функция f. Пока схему собирают, f никто не зовёт, поэтому к моменту вызова Hop давно объявлен. Когда разбор впервые доходит до поля next, узел зовёт f и запоминает ответ, второй раз функцию не зовут. Глубину дерева описаний это не меняет: оно конечное, в нём есть петля. А глубина данных может быть любой: тест этапа разбирает цепочку из тысячи перегонов. Ту же отсрочку ты делал руками в своём Гексе в уроке 30: функция вместо значения.
Проверка, которой нужен мир
Теперь книга башен. Знать, есть ли башня, схема сама не может: список башен меняется, лежит в базе и спрашивается по сети. Это сервис, как провод и журнал из EF5:
export class Registry extends Context.Service<
Registry,
{ readonly known: (code: TowerCode) => Effect.Effect<boolean> }
>()('magistral/Registry') {}
Проверки из урока 78 были чистыми функциями: получили значение, ответили. Здесь ответ это эффект. Для этого у Schema есть геттеры из модуля SchemaGetter, и среди них checkEffect: проверка, которая возвращает эффект. Отвечает она так же, как фильтр: true значит годится, строка значит беда с этим текстом.
export const KnownTower = TowerCode.pipe(
Schema.decode({
decode: SchemaGetter.checkEffect((code: TowerCode) =>
Effect.map(
Registry.use((registry) => registry.known(code)),
(known) => known || `башни ${code} нет в книге`,
),
),
encode: SchemaGetter.passthrough(),
}),
);
Schema.decode добавляет к схеме шаг: на разборе спросить книгу, на записи пропустить как есть. Шаг стоит после TowerCode, поэтому до книги доходят только коды правильного вида. На входе 'башня' разбор упадёт с код башни вида T-число, и книгу зря не потревожат. В тесте этапа это проверяет книга-шпион.
Главное видно в типе:
Schema.decodeUnknownEffect(KnownTower);
// (input: unknown) => Effect.Effect<TowerCode, Schema.SchemaError, Registry>
Registry попал в R разбора. Сервис, который понадобился одной башне в глубине поручения, поднимается по всей схеме до самого верха, как требование эффекта поднимается по yield* из урока 37. Разобрать поручение без книги не выйдет, и это видно ещё до запуска:
error TS2379: Argument of type 'Effect<{ readonly id: number & Brand<"FrameId">; readonly to: string & Brand<"TowerCode">; readonly path: Hop; }, SchemaError, Registry>' is not assignable to parameter of type 'Effect<{ readonly id: number & Brand<"FrameId">; readonly to: string & Brand<"TowerCode">; readonly path: Hop; }, SchemaError, never>' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties.
Type 'Registry' is not assignable to type 'never'.
Это знакомый по Сундуку отказ: runSync хочет пустой R, а там книга. Effect.provide(Registry.layer(['T-1', 'T-4'])), и разбор пошёл:
башни T-9 нет в книге
at ["to"]
Беда пришла с путём, как любая другая: explain из прошлого урока превратит её в to: башни T-9 нет в книге без единой новой строки.
Что ещё можно делать в checkEffect: всё, что умеет эффект. Ждать ответа по сети, брать время из Clock, ставить timeout из EF8. Поэтому проверки с миром в Schema называют асинхронными. Синхронный разбор такую схему не примет: Schema.decodeUnknownSync(KnownTower) не собирается, потому что у него нет места для сервисов, его DecodingServices обязан быть never.
Где тут дыра
const route = (input: unknown) =>
Effect.gen(function* () {
const dispatch = yield* Schema.decodeUnknownEffect(Dispatch)(input);
if (!(yield* Registry.use((registry) => registry.known(dispatch.to)))) {
return yield* Effect.fail('нет башни');
}
return dispatch;
});
В одном месте работает. Но поручения Академия принимает в трёх: ночью с провода, днём из конторы и из архива при перезапуске. Ринцвальд вставил if в первые два. Через месяц в третьем месте всплывает поручение в снесённую T-9, и тип Dispatch ничем не выдал, что оно не проверено. Хуже того, отказ 'нет башни' строкой не похож на остальные беды разбора: ни пути, ни места в explain.
Лечится тем, что проверка становится частью схемы. Тогда значение типа Dispatch нельзя получить, не спросив книгу, а забыть спросить нельзя: R не даст.
Магистраль, этап 7
Договор кадров учит маршрут любой длины и узнаёт у книги башен, существует ли адресат. Книга становится зависимостью разбора и видна в его типе.
Практика
Дальше
Вечером к Казимиру выстраивается очередь из посыльных. Каждый держит пустой лист и печать своей гильдии.
Копии договора, которые выводятся из одной схемы, ждут в следующем уроке.