Advent of Property Based Testing (PBT)
Что такое PBT - это тестирование свойств) Например имея функцию возрастающей сортировки массивов можно обнаружить что она обладать такими:
- длина результата до и после одинаковая
- каждый следующий элемент больше предыдущего
- массив содержит те же элементы что и до сортировки
Но хочется быть уверенным что свойства функции корректны для любых входных данных. Перебирать их все и все их комбинации - достаточно утомительное дело)
Поэтому придумали делать вот что - давайте напишем контракт генерации входных данных - и на конкретном срезе данных будем тестировать функцию - но не один раз а мильены-несчитанные - это конечно не бесконечно, но лучше чем писать руками - а при генерации этих самых срезов давайте еще вначале какие-нибудь специальные краевые случаи поставим - напрмиер для контракта чиселки выдадим
А теперь давайте к делу напишем подобный тест (потыкаться тут https://effect.website/play#f91ec803be7d)
Конечно писать мы будем с использованием Schema из Effect - иначе слишком просто)
Как же так - схема ж для сериализации-десериализации - но нет схема это просто описание контракта которое можно использовать и для генерации так называемых
В настоящее время нет схемы описания имени - по требованиям это любые сочетания англ букв в нижнем регистре. Но у любой схемы можно переопределить аннтацию генерации
Собираем контракт письма воедино
Раз уж затронули тему аннотаций то есть удобная штука
Давайте выведем так - "имя=возраст" - в таком формате принимается решение задачки - просто скопируем из консоли
Собственно тест
Видно, что
в тесте проверяем что функция реально сортирует как нам нужно
Что такое PBT - это тестирование свойств) Например имея функцию возрастающей сортировки массивов можно обнаружить что она обладать такими:
- длина результата до и после одинаковая
- каждый следующий элемент больше предыдущего
- массив содержит те же элементы что и до сортировки
Но хочется быть уверенным что свойства функции корректны для любых входных данных. Перебирать их все и все их комбинации - достаточно утомительное дело)
Поэтому придумали делать вот что - давайте напишем контракт генерации входных данных - и на конкретном срезе данных будем тестировать функцию - но не один раз а мильены-несчитанные - это конечно не бесконечно, но лучше чем писать руками - а при генерации этих самых срезов давайте еще вначале какие-нибудь специальные краевые случаи поставим - напрмиер для контракта чиселки выдадим
NaN +-Infinity - тем более по типам подходит. А когда тест упадет мы нарисуем с какими данными он упал - чтоб было легче воспроизвести)А теперь давайте к делу напишем подобный тест (потыкаться тут https://effect.website/play#f91ec803be7d)
Конечно писать мы будем с использованием Schema из Effect - иначе слишком просто)
Как же так - схема ж для сериализации-десериализации - но нет схема это просто описание контракта которое можно использовать и для генерации так называемых
fc.Arbitrary - тех самых произвольных значений которые удовлетворяют этому контракту с помощью модуля Arbitrary из effectfast-check(https://fast-check.dev/) - инструмент для PBT который и генерит тестовые данные огромными тучамиconst Age = S.Int.pipe(S.between(7, 77)) // хоба - описание возраста из задачи
const AgeArb = Arbitrary.make(Age) // теперь это контракт который понимает fast-check
В настоящее время нет схемы описания имени - по требованиям это любые сочетания англ букв в нижнем регистре. Но у любой схемы можно переопределить аннтацию генерации
fc.Arbitrary.const Name = S.String.annotations({
arbitrary: () => (fc) => fc.string({ unit: fc.constantFrom(..."abcdefghijklmnopqrstuvwxyz") })
}).pipe(
S.minLength(1) // далее накатываем фильтр на непустую строку
)
Собираем контракт письма воедино
const Letter = S.Struct({ name: Name, age: Age }).annotations({
pretty: () => ({ name, age }) => [name, age].join("=")
})
const Letters = S.Array(Letter) // массив писем
const LettersArb = Arbitrary.make(Letters)
Раз уж затронули тему аннотаций то есть удобная штука
Pretty - это для того чтоб указывать как сущность должна превращаться в строку (отладка, логирование и тд) для этого тоже есть аннотация. Давайте выведем так - "имя=возраст" - в таком формате принимается решение задачки - просто скопируем из консоли
Letter.annotations({
pretty: () => ({ name, age }) => [name, age].join("=")
})
const LetterPretty = Pretty.make(Letter) // (value: Letter) => string
Собственно тест
describe("day #1: should properly sort letters", () => {
fc.assert(
fc.property(lettersArb, (unsortedLetters) => {
it(() => {
const letters = sortLetters(unsortedLetters)
for (let i = 1; i < letters.length; ++i) {
const prev = letters[i - 1]
const curr = letters[i]
if (prev.age < curr.age) continue // properly ordered
if (prev.age > curr.age) throw new Error(`Invalid on age ${letterPretty(prev)} ${letterPretty(curr)}`)
if (prev.name > curr.name) throw new Error(`Invalid on name ${letterPretty(prev)} ${letterPretty(curr)}`)
}
})
}),
{ numRuns: 1000 } // количество прогонов
)
})
describe&it - функции для организации тестовfc.property - описание того как проверить свойствоfc.assert - запускатор проверки свойств - принимающий множество настроек (количество прогонов, seed ГСЧ и тд) - он даже пытается минимизировать входные данные котороые приводят к ошибки для более легкой отладкиВидно, что
fast-check можно достаточно бесшовно внедрить и в vitest и в jest - потомучто это просто функции в тесте проверяем что функция реально сортирует как нам нужно
Effect Documentation
Effect Playground
👍3⚡1
Khraks dev
Преследуя желание изучить и глубже понять, как работает fp-ts, за неимением объяснений и хоть какой-то документации я прошел несколько курсов по Haskell - на степике от Дениса Москвина, за что ему благодарен) (https://stepik.org/course/75/syllabus, https:…
Дочитал до 10 главы - в восторге от книги!
Нужно, конечно, держать в уме, что js однопоточный и некоторые примитивы не так полезны, как в многопоточной среде. Но книга отвечает на многие вопросы, которые задают в том же Discord и не освещены в документации. Вводит в темы постепенно, с показательными примерами кода - задает вопросы и отвечает на них, сначала с ошибкой, потом разбирается, что не так и как улучшить. В конце главы даются упражнения - но ответы и их разборы я не искал)
Планирую транслировать все примеры кода на
https://github.com/KhraksMamtsov/effect-zionomicon
Нужно, конечно, держать в уме, что js однопоточный и некоторые примитивы не так полезны, как в многопоточной среде. Но книга отвечает на многие вопросы, которые задают в том же Discord и не освещены в документации. Вводит в темы постепенно, с показательными примерами кода - задает вопросы и отвечает на них, сначала с ошибкой, потом разбирается, что не так и как улучшить. В конце главы даются упражнения - но ответы и их разборы я не искал)
Планирую транслировать все примеры кода на
Effect (благо это сделать достаточно легко) и выложить в репку. Поспособствовать ткскзть популяризации и освоению технологии.https://github.com/KhraksMamtsov/effect-zionomicon
👍5⚡1
Forwarded from Podlodka Podcast – анонсы и новости подкаста про IT
Podlodka #404 – Системы эффектов в языках программирования
Что общего у скорости вычислений, мутабельности, кеширования и исключений? Все это – сайд-эффекты, которые сопровождают результаты вычислений. Виталий Брагилевский показал нам невероятно прекрасную картину, которая открывается, если рассматривать все возможные эффекты как часть одной системы – программирование становится более простым, контролируемым и выразительным!
🎧 Слушать выпуск
👀 Смотреть выпуск
Что общего у скорости вычислений, мутабельности, кеширования и исключений? Все это – сайд-эффекты, которые сопровождают результаты вычислений. Виталий Брагилевский показал нам невероятно прекрасную картину, которая открывается, если рассматривать все возможные эффекты как часть одной системы – программирование становится более простым, контролируемым и выразительным!
🎧 Слушать выпуск
👀 Смотреть выпуск
🔥5 1
# FiberRef
В
При создании
Значения по умолчанию имеют смысл, чтобы не нарушать
Результат
Playground с демонстрацией
FiberRef в Zionomicon
В демо видно что после слияния дочерний
Для чего же может пригодиться
Его можно использовать как контекст — хранить уровень логирования, какие-то аннотации и дефолты
В файбере-обработчике http-запроса — это может быть какая-то контекстная информация об этом запросе, юзер, роль и тд.
Все дефолтные сервисы
Так что можно сказать что
Название
В этом логе видно что файбер один и тот же #0, а значение
В
Zionomicon как-то вскользь прошла тема FiberRef - но, как я обнаружил далее, это очень важная деталь имплементации самого Effect которая не упоминается в документации, поэтому хочу поделиться тем что накопал.FiberRef — Ref который привязан к конкретному файберу, является его локальным состоянием.// FiberRef.ts
declare const make: <A>(
initial: A,
options?: {
readonly fork?: ((a: A) => A) | undefined
readonly join?: ((parent: A, child: A) => A) | undefined
}
) => Effect<FiberRef<A>, never, Scope>
При создании
FiberRef можно указать стратегии копирования и слияния при fork-join файбера.Значения по умолчанию имеют смысл, чтобы не нарушать
fork-join identity закон файберов —Результат
fork и немедленного join файбера, должен быть идентичен результату синхронной работы программы{
fork: identity,
join: (_parent, child) => child
}
Playground с демонстрацией
FiberRef в Zionomicon
В демо видно что после слияния дочерний
FiberRef переопределяет родительский.Для чего же может пригодиться
FiberRef? Его можно использовать как контекст — хранить уровень логирования, какие-то аннотации и дефолты
В файбере-обработчике http-запроса — это может быть какая-то контекстная информация об этом запросе, юзер, роль и тд.
Все дефолтные сервисы
Effect предоставляются через этот механизм Так что можно сказать что
FiberRed — это такой альтернативный неявный способ передать контекст, он не влияет на типовой канал RНазвание
FiberRef отражает основное применение, но механизм гибче и опирается на состояние текущего выполнения — у файбера определен реджистри FiberRefs в который и складываются все FiberRef при создании. При fork-join файберов это реджистри клонируется и мерджатся согласно стратегиям которые задаются при создании рефов. Но можно изменить FiberRefs и текущего файбера — так как это вещь региональная, то есть ее можно переопределить. Например семейство Effect.locally* переопределяет FiberRef в оборачиваемом эффекте. // Effect.ts
export const locally: <A>(
self: FiberRef<A>,
value: A
): <B, E, R>(use: Effect<B, E, R>) => Effect<B, E, R>
В этом логе видно что файбер один и тот же #0, а значение
FiberRef временно переопределяется: const program = Effect.gen(function*() {
const fiberRef = yield* FiberRef.make(42)
// переопределяем 42 -> 46
yield* Effect.locally(fiberRef, 46)(Effect.gen(function*() {
const valueLocally = yield* fiberRef
yield* Effect.log({ valueLocally })
// INFO (#0): { valueLocally: 46 }
}))
const valueAfterLocally = yield* fiberRef
yield* Effect.log({ valueAfterLocally })
// INFO (#0): { valueLocally: 42 }
})## Патчи, Differ
В демо видно, что дочерний
Чтобы решить проблему используется подход как в контролях версий - давайте хранить патчи и накатывать их когда надо. Это знание хранит в себе
Можно сказать что это
Вот наивный пример как сделать
А этот более интересный с применением комбинаторов и конструкторов для примитивов - идея в том что кастомная сущность мапится на более элементарную структуру и обратно, но для этой простой структуры известен
- ZIO Doc — https://zio.dev/reference/state-management/fiberref
- Видео по доке — https://www.youtube.com/watch?v=zpwmsYce8KU
В демо видно, что дочерний
FiberRef переопределяет родительский при join. Но что будет если в FiberRef будет храниться не примитивное значение? Можем получить неожиданное поведение - например как тут потерялось "new A":const defaultSettings = new Settings("default", 0)
let defaultSettingsFiberRef = FiberRef.unsafeMake(defaultSettings)
const runnable = Effect.gen(function*() {
const ref = yield* defaultSettingsFiberRef
yield* Effect.log(ref)
// Settings("default", 0)
yield* Effect.zip(
FiberRef.update(defaultSettingsFiberRef, (x) => x.setA("new A")),
FiberRef.update(defaultSettingsFiberRef, (x) => x.setB(111111)),
{ concurrent: true }
)
const newRef = yield* defaultSettingsFiberRef
yield* Effect.log(newRef)
// Settings("default", 111111)
})
Чтобы решить проблему используется подход как в контролях версий - давайте хранить патчи и накатывать их когда надо. Это знание хранит в себе
Differ:interface Differ<in out Value, in out Patch> {
readonly empty: Patch
diff(oldValue: Value, newValue: Value): Patch
combine(first: Patch, second: Patch): Patch
patch(patch: Patch, oldValue: Value): Value
}
Можно сказать что это
Monoid<Patch>, который умеет находить Patch для Value (diff) и как-то их накатывать (patch). combine — ассоциативная операция (a*(b*c)=(a*b)*c), чтобы при различных порядках Fiber.join результат FiberRef не менялся. Вот наивный пример как сделать
Diff<Settings, ...>А этот более интересный с применением комбинаторов и конструкторов для примитивов - идея в том что кастомная сущность мапится на более элементарную структуру и обратно, но для этой простой структуры известен
Diff. В типе видно, что для рассчетов применяется более примитивный патч.Differ<Settings, readonly [(a: string) => string, (a: number) => number]>
- ZIO Doc — https://zio.dev/reference/state-management/fiberref
- Видео по доке — https://www.youtube.com/watch?v=zpwmsYce8KU
# Schedule ⏲️
В
-
-
-
Решил создать упрощенную модель на промисах, чтобы понять, как работает.
Давайте например напишем
Теперь
давайте теперь напишем
Все остальные комбинаторы похожи структурой, допустим комбинатор
Далее это передается в
Tак как это просто _описание_ расписания - подменив
Самая мощь - это
Люблю разбираться как устроены фичи под капотом - потомучто это знание не только углубляет понимание конкретной технологии, но и расширяет арсенал транслируемых навыков.
такой
Песок с разбором и полным кодом
Schedule — это абстракция для управления повторением эффектов, которая определяет, сколько раз и с какими интервалами операция должна выполняться или перезапускаться. Например в Effect она используется для определения расписаний при обработке ошибок (retry) или при обычном повторении эффектов(repeat)В
Effect они реализованы следующими компонентами:-
Schedule - начальное значение состояния + функция которая по некоторому состоянию и входным данным выдает тройку - [новое состояние, выходное значение, Decision]-
Decision - решение о продолжении выполнения - закончить или продолжать; спустя какое-то время или сразу же -
Driver - по сути стейтфул итератор - менеджит состояние Schedule - выставляя наружу только входные данные => выходное значение Решил создать упрощенную модель на промисах, чтобы понять, как работает.
interface Schedule<out Out, in In = unknown, in out S = any> {
readonly initial: S,
readonly step: (input: In, currentState: S) => [newState: S, Out, Decision],
}
type Decision = { tag: "Continue"; delay: 0 | number } | { tag: "Done" };
interface Driver<Out, in In, in out S> {
private state: S;
constructor(readonly schedule: Schedule<Out, In, S>)
next(input: In): Promise<Out>
}
Давайте например напишем
Schedule который будет стрелять 5 раза с интервалами как последовательность фибоначчи 0-1-1-2-3 секунд. Определим сначала unfold - операцию обратную reduce - из одного значения мы будем получать последовательность.const unfold = <A,>(initial: A, f: (a: A) => A): Schedule<A, unknown, A> =>
new Schedule<A, unknown, A>(initial, (_, state) => [
f(state), // новое состояние
state, // выходное значение
{ tag: "Continue", delay: 0 }, // мгновенное продолжение
]);
Теперь
fibonacci - передаем начальное значение и функцию шагаconst fibonacci = unfold<[number, number]>([0, 1], ([prev, cur]) => [cur,cur + prev])
давайте теперь напишем
map чтобы создавать новый Scheduler с новым выходным значением — дергаем оригинальный и воздействуем на его выходconst map = <Out, Out1>(f: (out: Out) => Out1) =>
<In, S>(schedule: Schedule<Out, In, S>) =>
new Schedule<Out1, In, S>(schedule.initial, (input, state) => {
const [_state, _out, _decision] = schedule.step(input, state);
return [_state, f(_out), _decision];
});
Все остальные комбинаторы похожи структурой, допустим комбинатор
intersect принимает два расписания и возвращает новый new Schedule в кишках которого выясняет, что вернут предыдущие и обрабатывает результат и возвращает свой - если какой-то Schedule решил остановиться то останавливается сам, если оба эмитят то эмитит наименьшее время.Далее это передается в
Driver который сохраняет состояние которое вернул Schedule и передает ему же на вход при следующей итерации, может сброситься до начального значения - потомучто эта информация зашита в сам Scheduler. Функция repeat в цикле запускает driver.next и запускает эффект, до тех пор пока не получит терминальную отмашку.Tак как это просто _описание_ расписания - подменив
Driver мы можем изменить поведение - давайте исполним все, например, игнорируя интервалы. или встроим в какой-нибудь rxjs? Самая мощь - это
Schedule - он композабелен. Да даже в этом примере видно как из простого unfold с различными маппингамми мы создали достаточно сложное расписание. Люблю разбираться как устроены фичи под капотом - потомучто это знание не только углубляет понимание конкретной технологии, но и расширяет арсенал транслируемых навыков.
такой
Schedule я бы не допетрил написать с нуля самостоятельно, a теперь понял смысл и оказалось, что это просто свистопляска вокруг одной функцииdeclare function step(input: In, currentState: S): [newState: S, Out, Decision];
Песок с разбором и полным кодом
🔥4
Написал PR в
https://github.com/standard-schema
Это такой стандарт, чтобы унифицировать интерфейсы различных библиотек для валидации - чтоб не писать тучу адаптеров под каждую. Стандарт еще только первой версии и описывает только процесс превращения "неизвесных" данных в "известные".
Функция, ради которой все затевалось, может возвращать
То есть, условно, моя библиотека для валидации может делать
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%. Основной проблемой был большой размер рантайма — видимо с этим хорошенько поработали. Надеюсь
- Хотят избавиться от
- Я так понимаю решили избавиться от сабтайпингов и оставить только возможность использовать
- Только я написал [статью на 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