# Effect 4.0
Закончились "Effect days".
Хочу немного пройтись по планам на след версию из этого твита (https://x.com/dillon_mulroy/status/1903401848083550429)
## Бетон
- "Smol" — по слухам редьюс бандлсайза 50-80%. Основной проблемой был большой размер рантайма — видимо с этим хорошенько поработали. Надеюсь
- Хотят избавиться от
- Я так понимаю решили избавиться от сабтайпингов и оставить только возможность использовать
- Только я написал [статью на dev.to как устроен
- от
## Обсуждается
-
-
- унификация нейминга функций по всему API
## Примерный план
- Beta через 3 месяца
- Ecosystem Port еще через 3 месяца
- Stable Release
- Effect 4 — первый LTS, поддержка Effect 3 в течение полугода после релиза
Чтож — рад был понаблюдать за ивентом (хоть и только через твиттер), увидеть фотки крутых инженеров, убедиться что gcanti не вымышленный персонаж
Если у кого-то есть комментарии — рад обсудить)
Закончились "Effect days".
Хочу немного пройтись по планам на след версию из этого твита (https://x.com/dillon_mulroy/status/1903401848083550429)
## Бетон
- "Smol" — по слухам редьюс бандлсайза 50-80%. Основной проблемой был большой размер рантайма — видимо с этим хорошенько поработали. Надеюсь
Effect будет более правдоподобным вариантом на фронте, как и Schema (сейчас она импортирует Effect для асинхронных трансформаций)- Хотят избавиться от
FiberRefs. Действительно вводило в заблуждение необходимость FiberRef при наличии механизма опциональных сервисов — так что по совету Max Brown лучше уже сейчас использовать Context.Reference- Я так понимаю решили избавиться от сабтайпингов и оставить только возможность использовать
yield*. Например Either<R, L> это вот такой подтип Effect<R, L>, что позволяло использовать Either в тех местах где ожидался Effect. Теперь будет возможно делать только yield* <Either>. Меньше способов стрельнуть себе в ногу.- Только я написал [статью на dev.to как устроен
Scheduler](https://dev.to/khraks_mamtsov/scheduling-in-effect-understanding-and-implementing-1j70), как его решили переписать полностью, как и многие core модули). Благо непоправимая польза от этого знания не померкнет и останется в копилке "transferable skills" - так что советую)- от
STM вообще решили избавиться в пользу каких-то из-коробочных транзакций???## Обсуждается
-
Either -> Result -
Exit<A, E> -> Result<A, Cause<E>> - унификация нейминга функций по всему API
## Примерный план
- Beta через 3 месяца
- Ecosystem Port еще через 3 месяца
- Stable Release
- Effect 4 — первый LTS, поддержка Effect 3 в течение полугода после релиза
Чтож — рад был понаблюдать за ивентом (хоть и только через твиттер), увидеть фотки крутых инженеров, убедиться что gcanti не вымышленный персонаж
XD. Круто что экосистема развивается и Effect все лучше приспосабливается к реалиям js. Если у кого-то есть комментарии — рад обсудить)
X (formerly Twitter)
Dillon Mulroy λ (@dillon_mulroy) on X
@lyovson @EffectTS_
👍6🔥2🤔1
Хоть
Для этого можно использовать такой прием - давайте помечать
В этом примере использовали брендирование
Более интересным решением будет использовать R-канал для наших целей —
Мораль такова:
если нужно пометить воркфлоу каким-то образом, то стоит использовать
TS-песочница с примером использования: https://tsplay.dev/m0kYaN
Effect и избавляет от проблемы раскраски функций. Все же может быть полезно отслеживать асинхронное выполнение, допустим, чтобы в решающий момент быть уверенным, что Effect.runSync не выкинет ошибку.Для этого можно использовать такой прием - давайте помечать
Effect меткой на тайп-левеле, а runSync ограничим таким образом, чтобы вызов эффекта с такой меткой был невалидным.const AsyncTypeId: unique symbol = Symbol.for("my-project/Async");
type AsyncTypeId = typeof AsyncTypeId;
interface Async {
[AsyncTypeId]: AsyncTypeId;
}
function markAsync<A, E, R>(
effect: Effect.Effect<A, E, R>,
): Effect.Effect<A, E, R> & Async {
return effect as any;
}
function runSync<A, E, Eff extends Effect.Effect<A, E>>(
effect: Eff extends Async ? never : Eff,
) {
return Effect.runSync(effect);
}
В этом примере использовали брендирование
& Async — но это наивный подход, потомучто он не будет работать с большинством встроенных функций Effect, так как они имеют сигнатуру не сохраняющую подтип самого эффекта, а оперируют только над его содержимым:declare const map: {
<A, B>(f: (a: A) => B): <E, R>(self: Effect<A, E, R>) => Effect<B, E, R>
}
Более интересным решением будет использовать R-канал для наших целей —
Effect<A, E, R | Async>. С такой сигнатурой не нужно оборачивать runSync, потомучто он требует Effect<A, E, never>. И можно ввернуть кастомный асинхронный раннер:function runMarkedAsync<A, E, R>(effect:
[Exclude<R, Async>] extends [never]
? Effect.Effect<A, E, R>
: Effect.Effect<A, E>
) { /* ... */ }
Мораль такова:
если нужно пометить воркфлоу каким-то образом, то стоит использовать
R-канал, а не брендированиеTS-песочница с примером использования: https://tsplay.dev/m0kYaN
www.typenoscriptlang.org
TS Playground - An online editor for exploring TypeScript and JavaScript
The Playground lets you write TypeScript or JavaScript online in a safe and sharable way.
👍2✍1
Khraks dev
# Effect 4.0 Закончились "Effect days". Хочу немного пройтись по планам на след версию из этого твита (https://x.com/dillon_mulroy/status/1903401848083550429) ## Бетон - "Smol" — по слухам редьюс бандлсайза 50-80%. Основной проблемой был большой размер рантайма…
Запись доклада Майкла о будущем Effect 4
https://youtu.be/nyvB6nRe5x0?si=Hh8jAseo6Xz66pDf
TLDR:
https://github.com/Effect-TS/effect-smol
- Уменьшился размер бандла:
-
-
-
-
-
- Многие модули имплементированы по новому - стали более производительными и уменьшились в размере - перешли от initial encoding (интерпретация операций) к final encoding (непосредственное выполнение) - думаю будет ниже порог входа для контрибьюторов, потомучто это более привычный стиль написания кода. Плюс кажется переписали все on top of Effect - раньше в initial encoding помоему для каждого модуля была своя пара интерпретатор-AST
-
- зависшие файберы теперь не прерывают процесс — что более интуитивно
- увеличина производительность
-
-
-
https://youtu.be/nyvB6nRe5x0?si=Hh8jAseo6Xz66pDf
TLDR:
https://github.com/Effect-TS/effect-smol
- Уменьшился размер бандла:
-
5.4Kb — базовый эффект с рантаймом-
17.3Kb — со схемой (было 50Kb)-
6.63Kb — со Stream-
Context.Reference - кажется единственным способом передавать неявные зависимости - RuntimeFlags и FiberRefs не будет -
Queue был прямым портом ZIO.Queue, теперь она будет Mailbox более производительная и фиче-комплит версия очереди- Многие модули имплементированы по новому - стали более производительными и уменьшились в размере - перешли от initial encoding (интерпретация операций) к final encoding (непосредственное выполнение) - думаю будет ниже порог входа для контрибьюторов, потомучто это более привычный стиль написания кода. Плюс кажется переписали все on top of Effect - раньше в initial encoding помоему для каждого модуля была своя пара интерпретатор-AST
Stream.fromIterable(100_000): 3.23s -> 170ms 20x faster - без переписания на go)-
STM встроили в Effect- зависшие файберы теперь не прерывают процесс — что более интуитивно
- увеличина производительность
-
Effect.cron теперь понимает таймзоны и добавляет CronParseError в E-канал-
Layer.effect - теперь предоставляет Scope по дефолту - Layer.scoped не нужен-
Effect.Service - возможно будет теперь единственным способом создавать DI-тегиYouTube
Building Effect 4.0 | Michael Arnaldi (Effect Days 2025)
Get support from the Effect community → https://discord.gg/effect-ts
Michael Arnaldi discusses the upcoming Effect 4.0, highlighting new features, performance improvements, and community feedback integration.
00:00 Intro & the hat story
02:12 Effect’s growth…
Michael Arnaldi discusses the upcoming Effect 4.0, highlighting new features, performance improvements, and community feedback integration.
00:00 Intro & the hat story
02:12 Effect’s growth…
🤓3👍2🔥2
Составил список открытых пропоузалов
• Async Context — R-channel & Context.Reference
• TS throws annotation — E-channel & expected errors
• Record & Tuple — Data module
• Composite — Equal trait
• Async Iterator helpers — Stream
• Explicit Resource Management — Scope
• pipeline operator — pipe function
• do-notation — Effect.Do | Effect.gen
Но более того — имплементация в
TC39 и соответствующих подходов в Effect — как по мне впечатляет. Особенно первый пункт:• Async Context — R-channel & Context.Reference
• TS throws annotation — E-channel & expected errors
• Record & Tuple — Data module
• Composite — Equal trait
• Async Iterator helpers — Stream
• Explicit Resource Management — Scope
• pipeline operator — pipe function
• do-notation — Effect.Do | Effect.gen
Но более того — имплементация в
Effect гораздо более композабельная и типобезопасная.👍6🤯6🔥1💩1
Type Hole
Это типа TODO, но которое НЕ заставляет компилятор гореть красным огнем корректности.
При компиляции GHC скажет
В TS тоже можно реализовать нечто подобное, без автореализации, конечно, но в некоторых местах может быть полезно то, что компилятор не будет подсвечивать код красным и выдавать безумную простыню ошибки.
Мне пригождается в написании
Вот тут и начинается веселуха - если ты написал эти функции неверно (а происходит это на протяжении всего времени ее реализации), то ts подчеркивает красным весь блок
Статья от несравненного Giulio Canti - автора
https://dev.to/gcanti/type-holes-in-typenoscript-2lck
У кого-то были юзкейсы, когда это могло быть полезно?
Это типа TODO, но которое НЕ заставляет компилятор гореть красным огнем корректности.
addTwoNumbers :: Int -> Int -> Int
addTwoNumbers x y = _
При компиляции GHC скажет
Found hole: _ :: Int. В каких-то языках с продвинутой системой типов (возможно сам Haskell, Idris - хз не разбирался) компилятор сам может подставить корректную реализацию! Там даже можно сначала описать типы, а потом "навайбкодить" компилятором реализацию))В TS тоже можно реализовать нечто подобное, без автореализации, конечно, но в некоторых местах может быть полезно то, что компилятор не будет подсвечивать код красным и выдавать безумную простыню ошибки.
const _ = <T>() => T {
throw new Error("Type hole");
}
Мне пригождается в написании
Schema.transform - это когда есть две схемы и их нужно смерджить в одну - AB transform CD = AD - но чтобы это сделать нужны две функции B => C & C => B.Schema.transform<AB, CD, {
encode, // B->C
decode, // C->B
}>
Вот тут и начинается веселуха - если ты написал эти функции неверно (а происходит это на протяжении всего времени ее реализации), то ts подчеркивает красным весь блок
{...} и при попытке навестись на аргументы функци - он сначала показывает всю ошибку (а они очень длинные порой) и только потом тип наводимого аргумента. Сначала я возвращал 123 as any чтобы заткнуть его, но тогда при наведении например на decode: C => B - я не понимал какой тип B должен вернуться. И тут я вспомнил про _() — возвращаем из обоих функций и имеем прелестнейший DX — при ховер на _() виден тип, который должен быть на месте этой дырки, и компилятор не вставляет палки в колеса.Статья от несравненного Giulio Canti - автора
fp-ts и Schemahttps://dev.to/gcanti/type-holes-in-typenoscript-2lck
У кого-то были юзкейсы, когда это могло быть полезно?
👍4❤🔥2🔥1
Недавно открыл для себя
Например есть аналог
Или рефакторнинг — "Переписать с
В README — способ подключения и список возможностей.
В Discord вижу, что идет движуха в проекте.
Кстати — может есть какие-то идеи, какие еще фичи можно туда добавить?
@effect/language-service - плагин для Typenoscript со специфичными для Effect диагностиками и рефакторингами.Например есть аналог
no-floating-promises:Effect.gen(function* () {
//Effect must be yielded or assigned to a variable.effect(3)
Effect.success(1);
})
Или рефакторнинг — "Переписать с
async-await на Effect.gen + yield*"const before = async () => {
const response = await fetch("");
const body = await response.json();
return body;
};
// Rewrite to Effect.gen with failures
const after = () =>
Effect.gen(function* () {
Effect.tryPromise({
try: () => fetch(""),
catch: (error) => ({ _tag: "Error1" as const, error }),
});
const body = yield* Effect.tryPromise({
try: () => response.json(),
catch: (error) => ({ _tag: "Error2" as const, error }),
});
return body;
});
В README — способ подключения и список возможностей.
В Discord вижу, что идет движуха в проекте.
Кстати — может есть какие-то идеи, какие еще фичи можно туда добавить?
🔥4🤩3
Раньше думал, что есть 2 механизма повторов эффектов -
Но после более детального изучения понял что их три - есть еще
Узнал я это пока ковырялся с PR, который решает вот такую вот задачку:
"Полезно было бы узнавать информацию о повторении в повторяемом эффекте!)"
Например, эффект падает после нескольких попыток получить запрос - мы хотим упасть и залогировать сколько времени мы стучались (как на стриншоте в комментах).
Тут у меня в голове все соединилось - я как раз изучил
Так что встречайте — великий и ужасный
PS Прикольно что пригодилось мое изучение
retry и repeat. Первый перезапускает упавшие эффекты, второй повторяет эффект переданный эффект после(!) инициального его запуска. Поэтому Effect.log(123).pipe(Effect.repeatN(2)) выполнится 3 раза - инициально + 2 повторения. Но после более детального изучения понял что их три - есть еще
scheduling - это когда мы запускаем эффект строго по расписанию (Effect.schedule).Узнал я это пока ковырялся с PR, который решает вот такую вот задачку:
"Полезно было бы узнавать информацию о повторении в повторяемом эффекте!)"
Например, эффект падает после нескольких попыток получить запрос - мы хотим упасть и залогировать сколько времени мы стучались (как на стриншоте в комментах).
Тут у меня в голове все соединилось - я как раз изучил
Schedule и FiberRef. Решение - просто вытаскивать из драйвера расписаний инфу о попытке и провайдить ее в обернутый эффект через Context.Reference. Но там где последняя попытка - там и все предыдушие - можно же сохранять инфу о всех вызовах! Но, как правильно заметил Тим Смарт, это привело бы к утечке памяти в случае бесконечных расписаний.Так что встречайте — великий и ужасный
Schedule.CurrentIterationMetadata (PR)Effect.gen(function* () {
const currentIterationMetadata = yield* Schedule.CurrentIterationMetadata
// ^? Schedule.IterationMetadata
// elapsed: Duration.zero,
// elapsedSincePrevious: Duration.zero,
// input: undefined,
// now: 0,
// recurrence: 0,
// start: 0
console.log(currentIterationMetadata)
}).pipe(Effect.repeat(Schedule.recurs(2)))
PS Прикольно что пригодилось мое изучение
FiberRef и Schedule.)✍3❤2
Хозяйке на заметку.
1. Эти два способа сужения типа не эквивалентны
2. Tакое API — unsound.
Eсли передать в качестве
PS
1. Эти два способа сужения типа не эквивалентны
function test<T>(testFn: T | (() => T)) {
if (testFn instanceof Function) {
testFn();
// ^? () => T
}
if (typeof testFn === "function") {
// @ts-expect-error: This expression is not callable.
testFn();
// ^? (() => T) | (T & Function)
}
}
2. Tакое API — unsound.
Eсли передать в качестве
T — функцию, то в рантайме невозможно будет отличить T от () => T PS
function useState<S>(initialState: S | (() => S)): ...
✍10🔥1
ExecutionPlan — появился в свежем релизе Effect@3.16 Интересно было посмотреть что за штучка. Кристализовался модуль в процессе разработки
@effect/aiСуть в том, что можно описать в плоском виде (просто массив) порядок сервисов, которые будет использовать эффект при неудаче.
То есть семантика буквально:
- попробовать сначала с этим сервисом:
- если успех, то идем дальше
- если нет - то другой сервис
- и тд
- пока не пройдемся по всему плану или не выйдем при первом успехе
Под капотом обычный советский
Effect.while, который по порядку провайдит сервисы и выставляет ретраи по расписанию. https://github.com/Effect-TS/effect/blob/main/packages/effect/src/internal/executionPlan.ts#L52Признаться, я ожидал нечто непонятное в реализации, но парни просто нашли достаточно запутанный кусок юзер-лэнд кода и запрятали за наглядную абстракцию
👍3🥱3
## Заинтересовался вот этой оптимизацией в
Суть в том что трансформеры
Идея оптимизации такова — а давайте, если схема использует только
Для удобства в схеме используется вот такой тип:
Оптимизация состоит из 3х частей:
- нужно сохранять тип трансформаций
- эти хелперы используются главным образом в функции ParseResult.go, которая отвечает за то чтобы обойти все AST-ноды схемы и выдать ParseResult.Parser— искомую функцию энкода-декода
- ParseResult.handleForbidden— функция в которой происходит конечное решение использовать рантайм или нет
Из ee реализации понятно, что чтобы ритуал состоялся необходимо:
- как намерение пользователя — запустить семейство
- так и возможность схемы — использование именно
В противном случае оптимизация перестает работать — и подрубается рантайм эффекта с синхронным планировщиком что, кажется, одно и то же, что и
Но так же важно помнить, что аннотации схемы
Schema:If you want more perf you can useParseResult.flatMapinstead ofEffect.genand get eager optimization skipping the Effect runtime.
Michael Arnaldi
Суть в том что трансформеры
Schema.transformOrFail должны возвращать Effect<unknown, ParseResult.ParseIssue, unknown>. Но например Either — это его подтип, который не требует вовлечения рантайма. Идея оптимизации такова — а давайте, если схема использует только
Either, то ее декодеры-энкодеры будем запускать без использования рантайма.Для удобства в схеме используется вот такой тип:
type ParseResult<A> = Either<A, ParseResult.ParseIssue>
Оптимизация состоит из 3х частей:
- нужно сохранять тип трансформаций
Either и не превращать его в Effect — за это отвечают функции в ParseResult с категорией optimisation: ParseResult.flatMap- эти хелперы используются главным образом в функции ParseResult.go, которая отвечает за то чтобы обойти все AST-ноды схемы и выдать ParseResult.Parser— искомую функцию энкода-декода
- ParseResult.handleForbidden— функция в которой происходит конечное решение использовать рантайм или нет
Из ee реализации понятно, что чтобы ритуал состоялся необходимо:
- как намерение пользователя — запустить семейство
validate/decode/encode c суффиксом Either/Option/Sync- так и возможность схемы — использование именно
ParseResult<A> при трансформацияхВ противном случае оптимизация перестает работать — и подрубается рантайм эффекта с синхронным планировщиком что, кажется, одно и то же, что и
Effect.runSyncНо так же важно помнить, что аннотации схемы
concurrent и batching НЕ будут применены для синхронных трансформаций, то есть тут трансформации первичны.X (formerly Twitter)
Michael Arnaldi (@MichaelArnaldi) on X
@dillon_mulroy if you want more perf you can use ParseResult.flatMap instead of Effect.gen and get eager optimization skipping the Effect runtime. Not that you'd need it
🔥3
Expandable hover
Хорошая фича, интересно как оно будет отбражать интерфейсы и юнионы?
https://x.com/drosenwasser/status/1939775110858383418?t=1rW9WKEh3GdniAtQ7_EGKQ&s=19
Хорошая фича, интересно как оно будет отбражать интерфейсы и юнионы?
https://x.com/drosenwasser/status/1939775110858383418?t=1rW9WKEh3GdniAtQ7_EGKQ&s=19
❤2
## Effect 4.0
Добавили модуль
Более удобный, как по мне, потомучто является по факту
PS У меня не однозначное мнение пока, пушто такой подход не дает тотальности (работает не одинаково на области определения), но нужно поюзать.
Так что
PS возможно реально лучше назвать
PSS кстати, вроде такая реализация лечит какое-то чудачество тайпскрипта — как вспомню отпишу
Добавили модуль
Filter — похож на Predicate. Но Predicate больше про нативную обертку предикатов и тайпгардов.
interface Filter<in Input, out Output = Input> {
(input: Input): Output | absent
}
const absent: unique symbol = Symbol.for("effect/Filter/absent")
Более удобный, как по мне, потомучто является по факту
filterMap — вместо Option.none() теперь Filter.absent и не нужен Option.some
declare function filterMap<A, B>(f: (value: A) => Option<B>): (arr: Array<A>) => Array<B>
declare function newFilter<A, B>(f: Filter<A, B>): (arr: Array<A>) => Array<B>
PS У меня не однозначное мнение пока, пушто такой подход не дает тотальности (работает не одинаково на области определения), но нужно поюзать.
newFilter(x => x)([1, 2])
// [1, 2]
newFilter(x => x)([Filter.absent, Filter.absent])
// []
Так что
Filter — прагматичный, Predicate — академичный и посмотрим что из этого получитсяPS возможно реально лучше назвать
FilterMapPSS кстати, вроде такая реализация лечит какое-то чудачество тайпскрипта — как вспомню отпишу
👍2❤1
## Effect 4.0
Вмерджили моe предложение об улучшении DX при работе с
Теперь можно задать
Пропихнуть в
https://github.com/Effect-TS/effect-smol/pull/269
Вмерджили моe предложение об улучшении DX при работе с
Redacted.Теперь можно задать
label при конструировании и при сериализации будет выведен он.
const redacted = Redacted.make('password')
const redactedWithLabel = Redacted.make('password', { label: 'MY_PASS' })
console.log(redacted) // <redacted>
console.log(redactedWithLabel) // <MY_PASS>
Пропихнуть в
Config не получилось, а хотелось бы чтобы label и имя переменной совпадали — Тим предположил, что это потенциальная утечка имени переменной окружения. Я думал об этом, но полагал, что opt in поведение более прагматично и прокатит, но нет — придется обходиться более явным вариантом.
Config.redacted('MY_PASS_ENV', { label: 'MY_PASS_NAME' })
// вместо
Config.redacted('MY_PASS')
MY_PASS_ENV — название имени переменной окруженияMY_PASS_NAME — label от Redactedhttps://github.com/Effect-TS/effect-smol/pull/269
🔥4
#DI как в Effect Pt1
После прочтения заметки вы будете понимать шутку.
Часто вижу, что хвалят DI в Effect.
Хочу поделиться дедовским рецептиком наколенной версии.
Во первых каждое вычисление должно трекать необходимую зависимость на уровне типов — в Effect это третий типопараметр R.
и так давайте вычислением будет функция которая чтобы посчитать
Давайте сразу аналог
А теперь запускатор этих самых вычислений:
Чтож бахнем хеллоу-ворлд:
А теперь давайте-ка вынесем наш логгер в сервис и доработаем функцию
Получаем ошибку — функция требует чтобы мы предоставили сервис-логгер
Давайте напишем функцию которая провайдит зависимость! Нужно держать в уме, что
Погнали пробросим зависимость:
А теперь давайте также будем выводить в консоль рандомные числа!
Теперь нужно связать эти две функции, чтобы вернулась новая. Понятно чтоб мы сначала получим рандомное чисто и потом его залогируем, но для этого нам понадобятся оба сервиса — рандом & логгер
Мааам можно мне: "Effect-like-DI"
Нет! У нас "Effect-like-DI" есть дома
Дома: Reader
После прочтения заметки вы будете понимать шутку.
Часто вижу, что хвалят DI в Effect.
Хочу поделиться дедовским рецептиком наколенной версии.
Во первых каждое вычисление должно трекать необходимую зависимость на уровне типов — в Effect это третий типопараметр R.
interface Computation<out Value, in Services extends {} = {}> {
(services: Services) => Value
}
и так давайте вычислением будет функция которая чтобы посчитать
Value ожидает на вход некие сервисы Services — их будем передавать прям мапой { serviceName: serviceImplementation }Давайте сразу аналог
Effect.sync сделаем — вычисление которое не требует никаких Services — Computation<Value>
const sync = <Value,>(cb: () => Value): Computation<Value> => () => cb()
А теперь запускатор этих самых вычислений:
const run = <A, R extends {}>(reader: {} extends R ? Reader<A, R> : never) =>
reader({} as R);
{} extends R — это условие как раз гарантирует что для корректного запуска функции ненужны никакие зависимости, собственно можно запустить функции просто вызовом, который потребует передать необходимые аргументы-сервисы или {} если ничего не требуетсяЧтож бахнем хеллоу-ворлд:
const helloDI: Computation<void> = sync(() => console.log("Hello, DI!"))
const result = run(helloDI) // => void
А теперь давайте-ка вынесем наш логгер в сервис и доработаем функцию
interface Logger {
log: (...args: ReadonlyArray<unknown>) => void
}
const ConsoleLogger: Logger = globalThis.console
const helloDI: Computation<void, { logger: Logger }> = (services) => services.logger.log("Hello, DI!")
run(helloDI) // Error
Получаем ошибку — функция требует чтобы мы предоставили сервис-логгер
Давайте напишем функцию которая провайдит зависимость! Нужно держать в уме, что
Services это по сути Key-Value мапка, а пробрасывать зависимости будем прям подмножеством этой мапки Partial<Services>, при этом из первоначальной мапки будем омитить то что пробросили Omit<Services, keyof Provided>
const provideServices =
<Value, Services extends {}>(computation: Reader<Value, Services>) =>
<Provided extends Partial<Services>>(provided: Provided): Reader<Value, Omit<Services, keyof Provided>> =>
(services) => {
const value = computation({ ...req, ...provided } as {} as Services);
return value;
};
Погнали пробросим зависимость:
const runnable = provideServices(helloDI)({ logger: ConsoleLogger })
run(runnable) // OK
А теперь давайте также будем выводить в консоль рандомные числа!
interface Random {
random: () => number;
}
const MathRandom: RandomService = globalThis.Math
const getRandom: Computation<void, { random: Random }> = (services) => services.random()
const logNumber = (num: number): Computation<void, { logger: Logger }> = (services) => services.logger.log(`Random: ${num}`)
Теперь нужно связать эти две функции, чтобы вернулась новая. Понятно чтоб мы сначала получим рандомное чисто и потом его залогируем, но для этого нам понадобятся оба сервиса — рандом & логгер
const andThen =
<Serivces1 extends {}, Value1>(computation1: Computation<Value1, Serivces1>) =>
<Value2, Serivces2 extends {}>(
fn: (a: Value1) => Computation<Value2, Services2>,
): Computation<Value2, Services1 & Services2> => {
return (r: Services1 & Services2) => {
const value1 = computation(r); // тут из общих сервисов возьмем только Services1
const computation2 = fn(value1);
const value2 = computation2(r) // тут из общих сервисов возьмем только Services2
return value2;
};
};
👍3
#DI как в Effect Pt2
Получаем вот что:
Можно заметить что мы заранее декларировали сервисы функкий в типах. Можно сделать более интересно — как
Соберем теперь все вместе
Вообще это подход к организации DI например в fp-ts. Там и в Haskell этот тип
Так что если нужен Effect-like DI — посмотрите на семейство модулей
Песочница https://tsplay.dev/NBOazm
А вы какой DI используете?
Получаем вот что:
const program: Computation<void, { logger: Logger, random: Random }> = andThan(getRandom)(logNumber)
const runnable = provideServices(program)({ logger: ConsoleLogger, random: MathRandom })
run(runnable) // OK
Можно заметить что мы заранее декларировали сервисы функкий в типах. Можно сделать более интересно — как
yield* MyServiceTag в Effect. Чтобы мы его просто читали и он сам проставлялся в зависимости функции! Получается мы хотим сделать такую функицю которая бы на вход принимала какой-то сервис и тут же бы возвращала его обратно в качестве результата вычисления — для дальнейшего использования
const tag = <Service extends {}>(): Reader<Service, Service> => {
return (service) => service;
};
Соберем теперь все вместе
const RandomTag = tag<{ random: Random }>();
const LoggerTag = tag<{ logger: ConsoleLogger }>();
const getRandom = flatMap(RandomTag)(
(services) => sync(() => services.random.random() ** 2),
);
const logNumber = (num: number) =>
flatMap(LoggerTag)((services) => sync(() => services.logger.log(num)));
const program = flatMap(getRnd)(logRnd);
const withConsole = provide(program)({
logger: ConsoleLogger,
});
const runnable = provide(withConsole)({
random: MathRandom,
});
run(program); // error
run(withConsole); // error
run(runnable); // ok
Вообще это подход к организации DI например в fp-ts. Там и в Haskell этот тип
Computation<> наызвается Reader. Так что идея не нова. В том же Effect под капотом Context та же мапка но на уровне типов зависимости трекаются через тип сумму |, а не произведение &. В начальных версиях было через & но от этого ушли из-за проблем с ts. Так что если нужен Effect-like DI — посмотрите на семейство модулей
Reader fp-ts.Песочница https://tsplay.dev/NBOazm
А вы какой DI используете?
www.typenoscriptlang.org
TS Playground - An online editor for exploring TypeScript and JavaScript
The Playground lets you write TypeScript or JavaScript online in a safe and sharable way.
👍4
#DI как в Effect Pt3
Теперь озадачился написанием аналога
Для реализации понадобится 2 вещи:
1. добавим на
Добавим конструктор из коллбека и обернем все функции, которые возвращают
2. И сам
Добавим типовые хелперы — обратите внимание
Недавно узнал интересный ts-прием — чтобы скастовать что-то на уровне типов можно просто сделать
Готово! Можно писать в императивном стиле.
Немного переработал
Песочница https://tsplay.dev/mxrP1w
Теперь озадачился написанием аналога
Effect.gen, чтобы можно было просто делать yield* вычислений и получать более читаемый код, вот искомый результат:
const program = gen(function* () {
const randomService = yield* RandomTag;
const randomNumber = random.random();
const logger = yield* LoggerTag;
logger.log(randomNumber);
return randomNumber;
});
Для реализации понадобится 2 вещи:
1. добавим на
Computation вот такой символ чтобы можно было делать yield* на объекте — в реализации будем просто return yield this — вызов .next() вернет само Computation<V, S>
interface Computation<out Value, in Services extends {} = {}> {
(services: Services): Value;
[Symbol.iterator](): Generator<Computation<Value, Services>, Value, any>;
}
Добавим конструктор из коллбека и обернем все функции, которые возвращают
Computation — sync, andThan, tag и тд
const make = <Value, Services extends {} = {},>(cb: (services: Services) => Value) : Computation<Value, Services> => {
const cb_ = cb as Computation<Value, Services>;
cb_[Symbol.iterator] = function* () {
return yield computation;
};
return cb_;
}
2. И сам
genДобавим типовые хелперы — обратите внимание
Services аккумулируются через &.Недавно узнал интересный ts-прием — чтобы скастовать что-то на уровне типов можно просто сделать
Extract<Some, CastTarget>, чем и воспользовался в Computation.Services:
declare namespace Computation {
export type Any = Computation<any, any>;
export type Value<C extends Any> = ReturnType<C>;
export type Services<C extends Any> = Extract<UnionToIntersection<Parameters<C>[0]>, {}>;
}
const gen = <C extends Computation.Any, A>(
f: () => Generator<C, A, unknown>
): Computation<A, Computation.Services<C>> => {
return make((services: Computation.Services<C>) => {
const iterator = f();
let state = iterator.next();
while (!state.done) {
const computation = state.value;
const value = computation(services);
state = iterator.next(value);
}
return state.value;
});
};
Готово! Можно писать в императивном стиле.
Немного переработал
tag — чтобы возвращался сразу сервис, а не кусок мапкиПесочница https://tsplay.dev/mxrP1w
www.typenoscriptlang.org
TS Playground - An online editor for exploring TypeScript and JavaScript
The Playground lets you write TypeScript or JavaScript online in a safe and sharable way.
🔥1