Khraks dev – Telegram
Написал PR в Effect для использования StandardSchemaV1 в Micro.
https://github.com/standard-schema
Это такой стандарт, чтобы унифицировать интерфейсы различных библиотек для валидации - чтоб не писать тучу адаптеров под каждую. Стандарт еще только первой версии и описывает только процесс превращения "неизвесных" данных в "известные".
/** Validates unknown input values. */
declare function validate(value: unknown) => Result<Output> | Promise<Result<Output>>;

Функция, ради которой все затевалось, может возвращать Promise и не принимает AbortSignal.
То есть, условно, моя библиотека для валидации может делать fetch зачем-то, но при этом не может его прервать.
🔥4👍1😱1💩1
# Effect 4.0
Закончились "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.
Если у кого-то есть комментарии — рад обсудить)
👍6🔥2🤔1
Хоть 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
👍21
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
- Уменьшился размер бандла:
- 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-теги
🤓3👍2🔥2
Составил список открытых пропоузалов TC39 и соответствующих подходов в Effect — как по мне впечатляет. Особенно первый пункт:

Async ContextR-channel & Context.Reference
TS throws annotationE-channel & expected errors
Record & TupleData module
CompositeEqual trait
Async Iterator helpersStream
Explicit Resource ManagementScope
pipeline operatorpipe function
do-notationEffect.Do | Effect.gen

Но более того — имплементация в Effect гораздо более композабельная и типобезопасная.
👍6🤯6🔥1💩1
Type Hole
Это типа 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 и Schema
https://dev.to/gcanti/type-holes-in-typenoscript-2lck

У кого-то были юзкейсы, когда это могло быть полезно?
👍4❤‍🔥2🔥1
Недавно открыл для себя @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 механизма повторов эффектов - 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.)
32
Хозяйке на заметку.

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
## Заинтересовался вот этой оптимизацией в Schema:

If you want more perf you can use ParseResult.flatMap instead of Effect.gen and 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 НЕ будут применены для синхронных трансформаций, то есть тут трансформации первичны.
🔥3
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔42
Expandable hover

Хорошая фича, интересно как оно будет отбражать интерфейсы и юнионы?

https://x.com/drosenwasser/status/1939775110858383418?t=1rW9WKEh3GdniAtQ7_EGKQ&s=19
2
## Effect 4.0
Добавили модуль 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 возможно реально лучше назвать FilterMap
PSS кстати, вроде такая реализация лечит какое-то чудачество тайпскрипта — как вспомню отпишу
👍21
## Effect 4.0
Вмерджили мо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_NAMElabel от Redacted

https://github.com/Effect-TS/effect-smol/pull/269
🔥4
#DI как в Effect Pt1

Мааам можно мне: "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 сделаем — вычисление которое не требует никаких ServicesComputation<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
Получаем вот что:

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 используете?
👍4
#DI как в Effect Pt3
Теперь озадачился написанием аналога 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>;
}

Добавим конструктор из коллбека и обернем все функции, которые возвращают Computationsync, 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
🔥1