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

Записка Гилта: код v3 и инструменты

middle~30 мин

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

Ринцвальд кладёт на стол Рыдкулла пачку листов из бумаг конторы Гилта. Сверху отчёт по башням, рабочий: контора гоняла его каждое утро до самого разоблачения. Код короткий и понятный. Только половины имён из него нет в Effect, который ты учил весь курс.

В уроке 118 ты переводил на Effect чужой код на Promise. Теперь чужой код уже на Effect, но на старом. Это проще: модель программы та же. Effect, Layer, Schema, Stream, Effect.gen и pipe никуда не делись. Поменялись имена, адреса модулей и несколько привычек.

Как узнать код v3

Вот кусок записки. Гилт собирает с башен плату за проезд писем:

// Записка Гилта, Effect 3
import { Effect, Either, Layer } from 'effect';

class Tolls extends Effect.Tag('Tolls')<
  Tolls,
  { readonly charge: (tower: string) => Effect.Effect<number, TollRefused> }
>() {}

const TollsLive = Layer.scoped(
  Tolls,
  Effect.gen(function* () {
    const ledger = yield* Effect.acquireRelease(openLedger, () => closeLedger);
    return { charge: (tower) => ledger.charge(tower) };
  }),
);

const collect = (towers: ReadonlyArray<string>) =>
  Effect.forEach(towers, (tower) => Effect.either(Tolls.charge(tower))).pipe(
    Effect.map((results) => results.filter(Either.isRight).map((r) => r.right)),
    Effect.tap(() => Effect.fork(Effect.log('сбор окончен'))),
  );

На 4.1 этот код не соберётся, и с первого взгляда видно почему. Приметы v3 короткие:

  • сервис объявлен через Context.Tag('...'), Effect.Tag, Effect.Service или Context.GenericTag; у Context.Tag и Effect.Tag строка ключа стоит до типов;
  • метод сервиса зовут прямо на классе: Tolls.charge(tower). Это аксессор, в v4 его нет;
  • в коде есть Either, catchAll, fork, zipRight;
  • импорты из @effect/platform, @effect/schema, @effect/rpc, @effect/cluster или @effect/cli;
  • Layer.scoped для слоя, который что-то открывает.

Своя примета есть у кода из бет 4.0, которые выходили с февраля 2026 года: адреса вида effect/unstable/http и, в ранних бетах, модуль ServiceMap вместо Context. Такой код ближе к 4.1, чем код v3, но тоже не соберётся.

Таблица переименований

Тот же кусок на 4.1:

// Записка Гилта, Effect 4.1
import { Context, Effect, Layer, Result } from 'effect';

class Tolls extends Context.Service<
  Tolls,
  { readonly charge: (tower: string) => Effect.Effect<number, TollRefused> }
>()('Tolls') {}

const TollsLive = Layer.effect(
  Tolls,
  Effect.gen(function* () {
    const ledger = yield* Effect.acquireRelease(openLedger, () => closeLedger);
    return Tolls.of({ charge: (tower) => ledger.charge(tower) });
  }),
);

const collect = (towers: ReadonlyArray<string>) =>
  Effect.forEach(towers, (tower) => Effect.result(Tolls.use((tolls) => tolls.charge(tower)))).pipe(
    Effect.map((results) => results.filter(Result.isSuccess).map((r) => r.success)),
    Effect.tap(() => Effect.forkChild(Effect.log('сбор окончен'))),
  );

Логика не поменялась ни на строку, поменялись только имена. Вот те, что встречаются чаще всего; каждое проверено на effect@4.1.0:

В коде v3В 4.1
Context.Tag('Id')<Self, Shape>()Context.Service<Self, Shape>()('Id')
Context.GenericTag<T>('Id')Context.Service<T>('Id')
Effect.Tag, Effect.ServiceContext.Service; слой пишешь сам через Layer.effect
аксессор Tolls.charge(x)Tolls.use((tolls) => tolls.charge(x))
Either, Either.right, Either.isRightResult, Result.succeed, Result.isSuccess
Effect.eitherEffect.result
Effect.catchAll, catchAllCause, catchAllDefectEffect.catch, catchCause, catchDefect
Effect.catchSomeEffect.catchFilter
Effect.fork, Effect.forkDaemonEffect.forkChild, Effect.forkDetach
Effect.zipRightEffect.andThen
Effect.tapErrorCauseEffect.tapCause
Layer.scopedLayer.effect (он сам даёт и закрывает Scope)
Option.fromNullableOption.fromNullishOr
Schema.decodeUnknown, ошибка ParseErrorSchema.decodeUnknownEffect, ошибка SchemaError
Schema.Literal('a', 'b')Schema.Literals(['a', 'b'])
Schema.Union(a, b)Schema.Union([a, b])
FiberRefContext.Reference
Effect.makeSemaphoreSemaphore.make
Stream.asyncStream.callback
effect/TestClockeffect/testing
@effect/platform, @effect/rpc, @effect/clustereffect/http, effect/persistence и другие, effect/rpc, effect/cluster

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

Две строки таблицы меняют не только имя. Effect.Service в v3 сам собирал слой Default и подключал зависимости; в v4 этого нет, слой ты пишешь явно, как делал с урока 47. А аксессоры убрали не из вредности: на дженерик-методе вроде get<T>(key) они теряли параметр типа, и T превращался в unknown.

Где тут дыра

Ошибка не пропала, она переехала. Компилятор молчит, потому что as any выключил ему глаза. Молчит и Effect Language Service, о котором ниже: правило про устаревшие имена смотрит на типы, а типа у any нет. Падает программа, и падает плохо. Аксессора на классе нет, вызов бросает TypeError, и сбор платы заканчивается не ошибкой TollRefused, а дефектом, который не поймает ни один catchTag.

Так выглядит почти любой перевод «по старой статье» или «по совету модели». Модели учились на годах кода v3, и в их ответах catchAll и Context.Tag встречаются чаще, чем имена 4.1. Правило простое: если перевод не собирается, читай ошибку и ищи имя в таблице. as any не переводит код, он прячет место, где перевод не закончен.

Что поменялось внутри

Пакеты @effect/platform, @effect/rpc, @effect/cluster, @effect/cli и другие въехали в сам effect. Отдельными остались только те, что тянут за собой платформу, провайдера или чужую технологию: @effect/platform-*, @effect/sql-*, @effect/ai-*, @effect/opentelemetry, @effect/atom-* и @effect/vitest. Ты пользовался этим весь курс, не замечая: effect/http в уроке 90 и effect/ai в уроке 113 пришли из того же пакета, что и Effect.

С нестабильными модулями история такая. В бетах 4.0 они жили по адресу effect/unstable/<модуль>, и путь сам предупреждал: осторожно. В 4.0.0 сегмент unstable убрали, а предупреждение переехало в документацию: у каждого модуля и каждого экспорта есть пометка @stability. unstable значит, что в минорной версии API может сломаться; модули ai, cli, cluster, http, http-api, persistence, reactivity, rpc, sql, workflow, а также process, socket, workers и другие. Путь стал короче, а осторожности нужно столько же. Поэтому во многих уроках курса стоит врезка «Нестабильно».

Рантайм файберов в v4 переписан ради памяти и скорости, а сам effect хорошо режется сборщиком: по замеру команды Effect минимальная программа весит 7,1 КБ после минификации и gzip.

Инструменты, которые читают за тебя

Читать имена по таблице полезно один раз. Дальше это делают инструменты.

Effect Language Service (@effect/language-service) это плагин TypeScript. Ставишь пакет и добавляешь его в tsconfig.json:

{
  "compilerOptions": {
    "plugins": [{ "name": "@effect/language-service" }]
  }
}

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

  • outdatedApi: имя из v3 в проекте на v4. На Effect.catchAll он прямо пишет, что это API v3;
  • floatingEffect: эффект создан и брошен, никто его не запустит;
  • missingStarInYieldEffectGen: yield без звёздочки в Effect.gen;
  • missingEffectContext и missingLayerContext: сервис, которого нет в требованиях;
  • catchUnfailableEffect: ловушка ошибок на эффекте, который не может упасть.

Часть правил выключена по умолчанию и включается в опциях плагина. Есть и рефакторинги: превратить async-функцию в Effect.gen, собрать слои по зависимостям, сделать из интерфейса класс Schema. Наведи курсор на yield*, и подсказка покажет все три параметра эффекта.

Обычный tsc плагинов не загружает, поэтому в CI правила молчат. Есть два выхода. effect-language-service diagnostics --project tsconfig.json печатает те же предупреждения отдельной командой. effect-language-service patch правит установленный TypeScript так, что tsc сам выдаёт диагностики Effect; команду кладут в prepare в package.json, чтобы правка переживала переустановку. Ещё одна команда, layerinfo, показывает, какой слой какие сервисы даёт.

@effect/tsgo это тот же набор правил для TypeScript 7, компилятора, переписанного на Go. Он умеет и то, что раньше делали линтеры: effect-tsgo oxlint --type-aware запускает Oxlint с правилами Effect, а effect-tsgo tsc запускает пропатченный компилятор, не трогая node_modules. Проверки идут прямо при сборке типов, и типы считаются один раз.

Нестабильно. Инструменты движутся быстрее библиотеки. Урок написан на @effect/language-service@0.87.5 и @effect/tsgo@0.63.0; второй требует TypeScript 7. Имена правил и команд сверяй с README своей версии. Модули с пометкой @stability unstable из effect@4.1.0 могут сломаться в минорной версии, стабильное ядро держит semver.

Разминка

Переведи записку Гилта целиком: рабочий отчёт по башням на Effect 3 должен собраться и работать на 4.1. Тесты смотрят на поведение отчёта и сводки и на их типы, а не на то, какими именами ты пользовался.

Практика

Глубже в архиве

Как линтер находит баги, которые пропускает компилятор, и как встроить его в CI, разобрано в уроке про качество кода курса по автоматизации тестирования.

Дальше

К вечеру записка Гилта собирается на 4.1, тесты зелёные, а в бумагах конторы на одну загадку меньше. Рыдкулл снимает мантию с манекена и вешает её на спинку твоего стула.