Недавно открыл для себя
Например есть аналог
Или рефакторнинг — "Переписать с
В 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