Комментарии 68
А простой и понятный код писать - слишком не спортивно?
const getTodo = ( id: number )=> httpClient.get( `/todos/${id}` ).json()
Это конечно моя ошибка что привожу такие примеры, что люди их интерпретируют потом так, типа смотрите! Я переписал ваш пример на Vanilla JS и все, пытаться найти для себя что то новое я не буду, и так все знаю
Promise
это по сути тот же `Effect<unknown, any, never>`
Проблема с Promise
в том, что он исполняется (eagerly executed) сразу когда функция getTodo
запускается
Мне ваш код нравится тоже, да, наглядно и красиво но он будет работать только в идеальных условиях, когда backend будет работать без ошибок по контракту и сеть никогда не упадет между вами и backend.
Разработчики кода понимают, в вызовах внешних сервисов может много чего пойти не так. Даже в вызовах функций нельзя доверять, потому что функции могут кинуть ошибку через throw
в любой момент и нужно быть готовым к такой ситуации.
Возвращаясь к вашему прекрасному коду:
что вы будете делать, если ваш
backend
будет иногда падать с 500 ошибкой и в таком случае нужно просто повторить запрос и ,например, пытаться не больше 3х раз с экспоненциальным разрывом между попытками а потом завершиться с ошибкой?Что делать если вы захотите кешировать результат и повторять запрос если проходит больше 5 минут, скажем?
Я могу много придумать сценариев но не вижу смысла, я привел 2 примера. Это не сложно написать код который будет кешировать и тп, просто это рутинный код который пишется на многих проектах, которые гарантируют определенный результат
И пишется такой рутинный где то хорошо, где то плохо, и от этого падает читабельность кода на уровне проектов. Такой рутинный код всегда будет, можно спрятать это под фреймворки, но это не мой путь написания хороших приложений
В моём коде нет Promise
. По поводу кейсов:
Первый реализуется в самом httpClient (иначе зачем он вообще нужен?)
Второй, вы не поверите, реализуется так:
const cachedTodo = cached( getTodo, minute(5) )
В коде нет явно написанного Promise
но подразумевается, или вы знаете как выполнять Http запросы синхронно? 😃
Ну а по поводу кейсов:
Первый реализуется в самом httpClient (иначе зачем он вообще нужен?)
я когда то пользовался каким то популярным клиентом, еще до знакомства Effect, и вот помню что он не поддерживал такую функциональность, нужно было подключать отдельные плагины к этой библиотеке, думаю можно не говорить что это overkill для такой задачи.
Ну а вообще, можно вынести такую логику на уровень клиента, вы правы, и Effect позволяет так делать, просто эта логика будет глобально для всех кто использует такого клиента.
import { Effect } from "effect"
import { HttpClient } from "@effect/platform"
export const getTodo = (
id: number
) =>
Effect.gen(function* () {
const client = yield* MySmartHttpClient;
const result = yield* client.get(`/todos/${id}`).pipe(
Effect.andThen((response) => response.json)
);
return result;
});
const MySmartHttpClient =
Effect.gen(function* () {
const client = yield* HttpClient.HttpClient;
client.pipe(
HttpClient.retry({
while: error => error._tag == "ResponseError" && error.response.status === 500,
times: 3
})
)
return client;
})
Второй, вы не поверите, реализуется так:
const cachedTodo = cached( getTodo, 5000 )
Ну во первых, я, как читатель этого кода, не понимаю сигнатуру функции cached.
Во вторых, вы написали тот самый рутинный код и сделали код менее читаемым, а вот как это было бы на Effect
import { Effect } from "effect"
import { HttpClient } from "@effect/platform"
const getTodo = (
id: number
) =>
Effect.andThen(
HttpClient.HttpClient,
httpClient =>
httpClient.get(`/todos/${id}`).pipe(
Effect.andThen((response) => response.json),
Effect.cachedWithTTL("5 seconds")
)
)
В коде нет явно написанного
Promise
но подразумевается, или вы знаете как выполнять Http запросы синхронно?
Разумеется, знаю:
const url = 'https://jsonplaceholder.typicode.com/users'
const users = $mol_fetch.json( url )
const names = users.map( user => user.name )
console.log( 'names count:', names.length )
я когда то пользовался каким то популярным клиентом, еще до знакомства Effect, и вот помню что он не поддерживал такую функциональность
Из примера было не очевидно, что это сторонняя библиотека, а не своя обёртка над своим API, которая в любом случае необходима.
я, как читатель этого кода, не понимаю сигнатуру функции
cached.
Как и любой другой функции, пока не посмотрите её сигнатуру.
Во вторых, вы написали тот самый рутинный код и сделали код менее читаемым, а вот как это было бы на Effect
Вы написали в 5 раз больше "нерутинного" кода, чем я "рутинного".
как выполнять Http запросы синхронно
Генераторы, не? :D
Хм, можете наглядный и простой пример привести? Я вот что то не знаю как сделать так, чтобы fetch
возвращал не Promise
а сразу его значение
Например, посредством техники suspense (как в Реакте)
А без React, на ванильном js чтобы работало в браузере или nodeJS? Можете прям сниппет здесь написать?
Никакую технику suspense я не видел, да и код его вообще не понятно на чем, похоже на $mol или какой то псевдокод. Никаких генераторов тоже не видно
он пишет код который похож на какой то реактивный фреймворк
На хабре уже есть очень хороший материал об этом https://habr.com/ru/articles/819005/
Там раскрывается порочность Promise и преимущества генераторов.
Не очень понял ваш посыл. Все поставленные вопросы решены из коробки, например, в RTKQuery, которая тоже является библиотекой. Таким образом, примеры и поставленные вопросы являются типовыми на которые уже есть классные решения из коробки.
Тогда вопрос, какие интересные особенности дает Effect за пределами типовых задач? Ну, или если на примерах типовых задач, то должно быть полное законченное решение без вот этих повисших в воздухе вопросов.
А пока на вид, еще одна библиотека из сотни другой уже выпущенной.
Не очень понял ваш посыл. Все поставленные вопросы решены из коробки, например, в RTKQuery, которая тоже является библиотекой. Таким образом, примеры и поставленные вопросы являются типовыми на которые уже есть классные решения из коробки.
Я посмотрел что такое RTKQuery
, вроде эта либа решает как раз проблемы с кешированием, реакцией на ошибки, восстановлением и тп. Но она заточена под р
Вы можете сказать еще что есть библиотека Zod
которая решает проблему с валидацией данных по схеме и тому подобное.
А пока на вид, еще одна библиотека из сотни другой уже выпущенной.
Все это - частные задачи, Effect же дает другой подход вообще в разработке через систему эффектов. Это не очередная TypeScript библиотека, если хотите сравнения с другими то это библиотека является портированной версией Zio библиотеки из Scala.
Вообще любая мощная библиотека дает "другой подход вообще к разработке". На слух это очередная библиотека со своим подходом.
Я не хочу сказать, что это плохо. Просто вы так написали, что эти типовые задачи там не решены, а дан подход к их решению. Типа создаешь эффект, а потом накидываешь на него нужную функциональность. И поэтому я отметил, что уже есть библиотеки, где эти проблемы решены из коробки.
Я понимаю, что во всем самому можно разобраться, но тогда зачем нужны отдельные статьи? Изюминка же какая-то должна быть. Например, в официальной документации информация где-то закопана или еще что-то.
И ниже народ тоже спрашивает про rxjs. И я вот тоже смотрю на эту статью и думаю, есть же rxjs
Я поторопился просто опубликовать и нужно было подумать над планом статьи.
Я больше года работаю в Effect экосистеме и для меня многие вещи понятны и очевидны, но это явно не так для других.
Я понимаю, что во всем самому можно разобраться, но тогда зачем нужны отдельные статьи?
Я опрос закинул на то, интересна эта тема или нет, через неделю посмотрю на результат и будет видно 😃
что вы будете делать, если ваш
backend
будет иногда падать с 500 ошибкой и в таком случае нужно просто повторить запрос и ,например, пытаться не больше 3х раз с экспоненциальным разрывом между попытками а потом завершиться с ошибкой?
Элементарно, эта логика пишется в вашей обертке для запросов к АПИ) Про то, что с помощью нее можно сделать глобальные ивенты before_request
, response
, error
, success_response
, перехватывать их и делать нужные вещи, показывать ошибку общую что-то пошло не так, на 401 коде кидать сразу на страницу логина и т.д. и т.п. Т.е. не использовать обертку для запросов к АПИ это по умолчанию безумие.
try {
const response = await new ApiReq('POST /api/items').withRetries(3).sendJSON(objData);
} catch (e) {
/* Полный контроль если надо отловить ошибку если все же ваши 3
заветных ретрая не увечались успехом и ты готов d throw В любой ситуации)
*/
}
Что делать если вы захотите кешировать результат и повторять запрос если проходит больше 5 минут, скажем?
Все так же элементарно, просто реализуйте это в обертке к АПИ. Пишется 1 раз в жизни, потратить нужно драгоценные 60 минут времени на ретраи и кэш.
try {
const response = await new ApiReq('POST /api/items').withCache(5).withRetries(3).sendJSON(objData);
} catch (e) {
/* Полный контроль если надо отловить ошибку если все же ваши 3
заветных ретрая не увечались успехом и ты готов d throw В любой ситуации)
*/
}
просто это рутинный код который пишется на многих проектах,
Он пишется 1 раз в жизни) Зачем такая наглая ложь) Потом копи паст с одного проекта в другой, ну либо в виде npm пакета подключается например
И пишется такой рутинный где то хорошо, где то плохо, и от этого падает читабельность кода на уровне проектов.
Код из примеров выше в этом комментарии плохо читается? С первого взгляда не ясно что он делает? Он рутинный? о_О
Такой рутинный код всегда будет, можно спрятать это под фреймворки, но это не мой путь написания хороших приложений
Ваш путь прятать его под конкретный фреймворк/библиотеку, а так да, это не ваш путь конечно прятать что-то)) Что за лицемерие на лицемерии?)
но это не мой путь написания хороших приложений
Ваш путь писать лапшекод такого вида:
const getTodo = (
id: number
): Effect.Effect<
unknown,
HttpClientError | TimeoutException
> =>
httpClient.get(`/todos/${id}`).pipe(
Effect.andThen((response) => response.json),
Effect.scoped,
Effect.timeout("1 second"),
Effect.retry({
schedule: Schedule.exponential(1000),
times: 3
}),
Effect.withSpan("getTodo", { attributes: { id } })
)
Жесть) Ещё один шаг и вы готовы к максимально убогому и вырвиглазному write-only once RxJS коду, хорошо подумали?)
Элементарно, эта логика пишется в вашей обертке для запросов к АПИ
Ну если вам не жалко и интересно тратить свое время на написание оберток, а не решать непосредственно сами задачи и спать спокойно ночью, то это ваш выбор 😀
Все так же элементарно, просто реализуйте это в обертке к АПИ. Пишется 1 раз в жизни, потратить нужно драгоценные 60 минут времени на ретраи и кэш.
Если вы всю жизнь пользуетесь одним http клиентом и он правильно работает со всеми возможными функциями где он используется, то да, ваш вариант имеет место быть. Да вы можете оформить это в NPM пакет, выложить в opensource но ваш клиент будет все равно ограниченным.
Что если на одном проекте нужно будет кеширование, а на другом нет? Или на разных проектах будут разные стратегии восстановления по ошибкам? Будете изобретать свой axios? Или будете городить функцию конструктор для вашего клиента? Удачи вам в этом, серебряных пуль как и идеальных http клиентов не бывает
Я ,например, много раз писал обертки на обертки и мне это надоело 🥲 Интереснее решать реальные задачи и не переизобретать колесо чисто технических задач
Ваш путь прятать его под конкретный фреймворк/библиотеку, а так да, это не ваш путь конечно прятать что-то)) Что за лицемерие на лицемерии?)
Я не понял в чем тут лицемерие, я имел ввиду что фреймворки скрывают такие детали чтобы разработчики писали бизнес код а не выбирали какая retry стратегия лучше.
Код использованием Effect является декларативным, он ничего не скрывает. Когда смотришь на эффект то видишь все.
Ваш же код, где использовался cached
, скрывает имплементацию, только название cached
говорит о том что это про кеширование. А перед кешированием может быть еще таймаут. И кеширование бывает разное, например мы хотим делать invalidate по истечению времени. И вот в таком случае смотришь на ваш cached
и не понимаешь детали
Ваш путь писать лапшекод такого вида:
Жесть) Ещё один шаг и вы готовы к максимально убогому и вырвиглазному write-only once RxJS коду, хорошо подумали?)
У вас от количества try catch и await глаза не заболели когда писали свои обертки?
И еще, у вас в catch будет ошибка типа unknown, то есть если вы захотите написать разное поведение для разных случаев то вам дорога только в instance of. Но конечно вы можете все ошибки смешать в кучу и на любые ошибки реагировать одинаково.
Effect позволяет на разные типы ошибок запустить разную логику по восстановлениям с этих ошибок
Ну если вам не жалко и интересно тратить свое время на написание оберток
Т.е. быть настоящим разработчиком. Нет не жалко. Благодаря таким как я, такие как вы, пользуются тем, что мы написали, потратив свое время.
Код использованием Effect является декларативным, он ничего не скрывает
Не скрывает?
Т.е. Effect.andThen
это пустая функция заглушка?
Т.е. Effect.retry
это пустая функция заглушка?
Ну и так далее.
я имел ввиду что фреймворки скрывают такие детали чтобы разработчики писали бизнес код
И в чем разница?) Если ваши Effect.retry
и т.п. так же скрывают детали)
У вас от количества try catch и await глаза не заболели когда писали свои обертки?
Нет)
И еще, у вас в catch будет ошибка типа unknown
В 99.9% случае этого достаточно) Когда ловишь ошибку в запросе к АПИ, то в 99.9% случаев именно она там и будет, а не ошибка что сеть отвалилась) В остальных 0.1% нужны конкретные детали ошибки и их достать тоже не проблема
Почему вам нравится new A().effect1().effect2().......effectN()
но не нравитсяnew A(A.effect1(), A.effect2(), ..... A.effectN())
Мне со стороны кажется, что только в этом разница, нет?
Не совсем понял про что вы говорите, можете сказать про что именно вы хотите подчеркнуть разницу?
В Effect много что поддерживает Dual Api
https://effect.website/docs/code-style/dual/
В копилку плюсов Effect(как и функционального программирования) это то, что у тебя ошибка типизирована, а не выбрасывается куда-то в код. Представим такую "нереалистичную ситуацию", когда в библиотеке забыли описать что метод/функция оказывается делает throw в каких-то ситуациях, а наш код, конечно, же не делает try/catch. В случае эффекта, ты не кидаешь ошибку, а возвращаешь и метод/функция уже типизирована ошибками самим Effect и ты гарантированно обработаешь ошибку
Да, это заслуга TypeScript и его способности выводить типы.
Effect был бы не таким привлекательным если бы не было языка со строгой статической типизацией
Особенно иронично, что Effect.andThen
был добавлен именно для того, чтобы не обрабатывать ошибку.
Нет, Effect.andThen
работает в случае успеха предыдущего эффекта.
Иронии нет никакой.
https://effect.website/docs/error-management/expected-errors/#short-circuiting
И не работает в случае ошибки, о чём и речь - вы её не обработали.
Вот так вот насмотрятся глупых шортсов на Ютубе и потом ретранслируют эти глупости дальше, даже не пытаясь критически осмыслить полученную информацию.
Забавно, но в Java пришли к выводу, что checked exceptions — это плохо
Очередная библиотека для реактивного программирования, а чем она лучше того же RxJS? Быстрее? Понятнее? Или всё вместе?
Я в общем-то не любитель реактивного программирования, но с простотой чтения кода соглашусь, а вот написание кода очень сомнительно, по сути каждая такая библиотека - это отдельный язык внутри языка и буквально приходится сидеть с документацией, выискивая - а какая же функция мне тут нужна. Обычные синтаксические конструкции того же JavaScript просто так не возьмёшь, потому что другая парадигма, и иногда их использовать не получится в принципе
Effect это не для реактивного программирования а скорее функционального.
Очередная библиотека для реактивного программирования, а чем она лучше того же RxJS? Быстрее? Понятнее? Или всё вместе?
Если я не ошибаюсь, то в реактивном программировании главным образом используются стримы и с двух разных сторон один посылает сообщения а другой их читает. И работа с API этих стримов выглядит так же как пишут код Scala разработчики, например. Мне кажется что функциональное программирование включает в себя и реактивное.
Effect использует pipe функцию которая выглядит как стрим но из из такого стрима можно выйти с типизированной ошибкой в любой момент. Эффект это как бы стрим, но не стрим как в реактивном программировании.
по сути каждая такая библиотека - это отдельный язык внутри языка и буквально приходится сидеть с документацией
это не отдельный язык внутри языка. Да, у Effect богатый API набор но все ведь написано на TypeScript, и все функции Effect имеют довольно примитивные названия, типа sleep, repeat, catch, andThen, die
Это не RxJS где тебе даются кубики и без документации с этим не разберешься а что каждый кубик отдельно делает.. Просто с RxJS я знаком поверхностно и давно, может ошибаюсь
Effect использует pipe функцию которая выглядит как стрим но из из такого стрима можно выйти с типизированной ошибкой в любой момент. Эффект это как бы стрим, но не стрим как в реактивном программировании.
Вот вам пример кода из моего текущего проекта, в котором к сожалению используется RxJS. Найдёте отличия?)

но из из такого стрима можно выйти с типизированной ошибкой в любой момент
А покажите как конкретно вы обработаете ошибки вот в таком коде, который вы показывали, сейчас он вообще без отлова и обработки ошибок. Прям полноценный код чтобы был. Т.е. прям обработка если httpClient отвалился и если таймаут вышел. С учетом вызова этой функции и получении из нее данных.
Скрытый текст
const getTodo = (
id: number
): Effect.Effect<
unknown,
HttpClientError | TimeoutException
> =>
httpClient.get(`/todos/${id}`).pipe(
Effect.andThen((response) => response.json),
Effect.scoped,
Effect.timeout("1 second"),
Effect.retry({
schedule: Schedule.exponential(1000),
times: 3
}),
Effect.withSpan("getTodo", { attributes: { id } })
)
import { HttpClient } from "@effect/platform"
import { Effect, Schedule } from "effect"
declare const httpClient: HttpClient.HttpClient
const getTodoWithRecover = (
id: number
) =>
httpClient.get(`/todos/${id}`).pipe(
Effect.andThen((response) => response.text),
Effect.scoped,
Effect.timeout("1 second"),
Effect.retry({ // будет 3 раза пытаться с любой ошибкой
schedule: Schedule.exponential(1000),
times: 3
}),
Effect.withSpan("getTodo", { attributes: { id } }),
Effect.catchTags({ // это отработает когда эффект с ошибкой завершился, даже retry не помог
TimeoutException: () => Effect.succeed("timed out"),
ResponseError: (error) => Effect.succeed(`response error. status = ${error.response.status}`),
RequestError: (error) => Effect.succeed(`request error. url = ${error.request.url}`)
})
)
// такой эффект возвращает теперь getTodo
// const getTodo: (id: number) => Effect.Effect<string, never, never>
const getTodoWithoutRecover = (
id: number
) =>
httpClient.get(`/todos/${id}`).pipe(
Effect.andThen((response) => response.json),
Effect.scoped,
Effect.timeout("1 second"),
Effect.retry({ // будет 3 раза пытаться с любой ошибкой
schedule: Schedule.exponential(1000),
times: 3
}),
Effect.withSpan("getTodo", { attributes: { id } }),
)
// const getTodoWithoutRecover: (id: number) => Effect.Effect<unknown, HttpClientError | TimeoutException, never>
// HttpClientError это union из двух типов RequestError, ResponseError
А в чем принципиальная разница тогда с instanceof
о котором вы негативно отзывались? Вот полностью типизированная обработка ошибок у промиса:
try {
const response = await apiRequest(...);
} catch (e) {
if (e instanceof TimeoutException) console.log("timed out")
if (e instanceof ResponseError) console.log(`response error. status = ${e.response.status}`)
if (e instanceof RequestError) console.log(`request error. url = ${e.request.url}`)
}
Выглядит по сути точно так же как у вас.
Effect.catchTags({ // это отработает когда эффект с ошибкой завершился, даже retry не помог
TimeoutException: () => Effect.succeed("timed out"),
ResponseError: (error) => Effect.succeed(`response error. status = ${error.response.status}`),
RequestError: (error) => Effect.succeed(`request error. url = ${error.request.url}`)
})
Да и делает все тоже самое
По моему скромному мнению первый код более нативный и понятный для глаза. И чем ближе к нативности, тем больше вероятность того, что код не будут переписывать из-за обновления api библиотеки.
Суть в том что в вашем случае вы можете забыть какой тип ошибки, вам нужно знать класс ошибки чтобы пользоваться оператором instance of
в случае с effect работает typescript inference и все возможные ошибки аккумулируются в эффекте и все они известны в compile time
И тут вдруг в рантайме прилетает какой-нибудь SyntaxError или TypeError...
Обожаю функциональщиков за их талант игнорировать реальность..
А разве для эффекта не вручную пишите дженерик со всеми типами ошибок? Как effect поймет если ошибка, например, кастомная? Он про статическую типизацию.
В случае с instanceof вы получаете как статическую проверку типа, так и ещё и проверку в рантайме.
Вручную не пишется, все ошибки отслеживаются Effect и из них делается union.
Например вот тут, todo это эффект, который не имеет смысла, просто показал как Effect отслеживает ошибки, он может завершиться либо Error1, либо Error2.
import { pipe, Data, Effect } from "effect"
export class Error1 extends Data.TaggedError("Error1")<{}> {}
export class Error2 extends Data.TaggedError("Error2")<{}> {}
const todo =
pipe(
Effect.fail(new Error1),
Effect.andThen(
Effect.fail(new Error2)
)
)
// const todo: Effect.Effect<never, Error1 | Error2, never>
Но если хотите то всегда можно явно написать тип.
https://effect.website/docs/error-management/expected-errors/#error-tracking
Как effect поймет если ошибка, например, кастомная?
Хороший вопрос 🙂
У кастомных ошибок должно быть свойство _tag: string, в примере выше этот тег проставляется в классе Data.TaggedError
https://effect.website/docs/error-management/yieldable-errors/
Effect это еще идея про то как можно и нужно писать программы, которые легко... читать
когда функция
getTodo
возвратила эффект то она не выполнит http запрос, она лишь вернет эффект, который умеет посылать http запрос
И откуда же должно стать очевидно, что httpClient.get()
не выполняет запрос, а возвращает эффект? Разумеется, в проекте, который пишется с нуля и не содержит внешних зависимостей, это более-менее очевидно, но что если большой проект решит постепенно переползать на Effect? Или это проект, переданный на поддержку другой команде. У большинства современных разработчиков скорее будет предположение, что httpClient.get()
выполняет запрос и возвращает промис.
И откуда же должно стать очевидно, что
httpClient.get()
не выполняет запрос, а возвращает эффект?
Effect это TypeScript библиотека
const getTodo = (
id: number
): Effect.Effect<unknown, HttpClientError> => // Вот отсюда, Typescript тип
httpClient.get(`/todos/${id}`).pipe(
Effect.andThen((response) => response.json)
)
У большинства современных разработчиков скорее будет предположение, что
httpClient.get()
выполняет запрос и возвращает промис.
Ну если такие TypeScript разработчики не видят тип после двоеточия то им никакие эффекты вместе с промисами не помогут 😀
Для этого нужно сначала посмотреть определение функции, а в условном листинге гитхаба/гитлаба этого видно не будет.
А в getTodo явного специфицирования типа может и не быть, ведь TS может его самостоятельно вывести, а разработчики ленивые.
Это справедливое замечание
Просто этот пример немного некорректный еще, потому что нигде не указано а что за httpClient
, просто для наглядности его вынесли за скобки.
Вообще полный код будет выглядеть так и будет сразу понятно что возвращается эффект, даже если читаешь pull request с кодом
import { Effect } from "effect"
import { HttpClient } from "@effect/platform"
const getTodo = (
id: number
) =>
Effect.andThen(HttpClient.HttpClient, httpClient =>
httpClient.get(`/todos/${id}`).pipe(
Effect.andThen((response) => response.json)
)
)
// const getTodo: (id: number) => Effect.Effect<unknown, HttpClientError, HttpClient.HttpClient | Scope>
ну или можно так написать, это более читаемый способ, использовать функцию генератор
import { Effect } from "effect"
import { HttpClient } from "@effect/platform"
const getTodo = (
id: number
) =>
Effect.gen(function* () {
const client = yield* HttpClient.HttpClient;
const result = yield* client.get(`/todos/${id}`).pipe(
Effect.andThen((response) => response.json)
);
return result;
});
// const getTodo: (id: number) => Effect.Effect<unknown, HttpClientError, HttpClient.HttpClient | Scope>
Хочу заметить, что польза совсем не раскрыта.
На том же сайте Effects есть более убедительные примеры.
Ну и раз это статья на Хабре с умным контингентом было бы уместно разобрать чем это лучше или хуже Rx.
Да, тема не раскрыта, первый блин комом как говориться, я просто не нашел даже упоминания про Effect и хотел побыстрее исправить это и просто немного заинтересовать людей, поэтому я приложил ссылки.
Сам главный сайт Effect намного круче чем моя статья, я не спорю.
const getTodo = (
id: number
): Effect.Effect<
unknown,
HttpClientError | TimeoutException
> =>
httpClient.get(`/todos/${id}`).pipe(
Effect.andThen((response) => response.json),
Effect.scoped,
Effect.timeout("1 second"),
Effect.retry({
schedule: Schedule.exponential(1000),
times: 3
}),
Effect.withSpan("getTodo", { attributes: { id } })
)
Емнип, в TS есть декораторы, которые ИМХО понятней всех этих "Effect", очень интересно почему их не радуют создатели этой библиотеки?
Ну во первых, декораторы находятся уже несколько лет как в experimental stage и все никак не дойдут до стабильной.
Во вторых, создатели Effect взяли и берут из TypeScript все возможное на текущий момент и я допускаю что может декораторы тоже будут где нибудь использоваться в экосистеме Effect если это будет иметь практический смысл.
Я не использовал декораторы в TypeScript но прекрасно понимаю идею, как они применяются в Java Spring например, это просто магия, код может падать в runtime просто если забыть добавить какой нибудь декоратор. Это просто Runtime механизм, а Effect это про Compile time
Мое мнение насчет декораторов что они не будут использоваться в Effect до тех пор, пока декораторы не будут нести информацию по типам на уровне компиляции. Я не слежу за декораторами, могу ошибаться
А можно подробней про compile time? Те если я верно понимаю, что использование эффекта облегчает проброску ошибок нужного типа? Или? Просто на данный момент она для меня выглядит как просто набор фий на промисах. Те при вызове effect просто создаётся промис который возвращается и все + набор оберток этих промисов. Это не так?
А можно подробней про compile time? Те если я верно понимаю, что использование эффекта облегчает проброску ошибок нужного типа?
Да, все ошибки пробрасываются на уровень типа эффекта и они известны на уровне написания кода.
Таким образом мы можем восстанавливаться с определенных ошибок, делать точечную логику и тп.
https://effect.website/docs/error-management/expected-errors/#error-tracking
То есть TypeScript это не просто проверяет синтаксические ошибки а он помогает писать fault tolerant код.
Просто на данный момент она для меня выглядит как просто набор фий на промисах.
Промисы это плохо так как они не типизированы в плане возможных ошибок и исполняются сразу как только создаются
и они вообще не используются, точнее используются только на крайних точка приложения (где мы например запускаем наше приложение).
В конечном итоге эффекты исполняются в файберах, это легковесные промисы. Файберы это как Future в Scale или Корутины в котлине
Вот тут рассказывают про Effect oriented программировании и что такое эффекты вообще
https://www.youtube.com/watch?v=252slbrmk8M&t=4157s
Спасибо за ответ.
По промисам - имел ввиду фабрику промисов - т.е. вызов создает фю, которая потом при вызове отдаст промиз, возвращаемое значение из промиза (опять-же если верно помню) можно типизировать в TS - и оно точно также будет подсвечиваться и пониматься IDE.
Не очень понимаю про что вы 😅
Повторюсь что с Promise<any> невозможно пробрасывать ошибки и компилятор никогда не догадается что там за тип, так как такого Generic параметра просто не задумано.
Promise<number> это Effect<number, any, never>
то есть ошибка может быть типа any
то есть вообщем любым значением
Effect<A, E, R> это типа "умный" Promise, у которого в типе есть информация о типе с успешним значением A, или возможной ошибкой типа E, и которому требуется контекст типа R, для того чтобы система типов (Effect runtime) мог запустить этот промис
Промисы вполне спокойно типизируются..
У меня связка react query + axios, это чудо выглядит так:
// Прим. api.ts
export const fetchAllUser = async (): Promise<AxiosResponse<UserType[]>> => {
return await axiosInstance.get(`/user`)
}
// Прим. UserList.tsx
const UserList = () => {
const { data, isFetched, isError } = useQuery({ queryKey: ['users'], queryFn: fetchAllUser })
if (isError) return <p>Упс..</p>
if (!isFetched) return <p>Загрузка..</p>
return (
<div>
{data?.data?.map((value) => <User key={value.id} value={value} />)}
</div>
)
}
(Код специально упрощён)
Какой профит я смогу получить, использовав Effect в данном случае и есть ли в этом смысл? (Доку ещё не открывал, думал статья даст больше ответов и информации для размышления..)
Сам по себе Promise не типиризует ошибки, он типизирует только успешный результат. То есть Promise<A> это Effect<A, any, never>
В вашем примере React Query больше похож на одно из применений Effect, мне нравится.
Я немного знаю по React Query но думаю что он заточен под работы с Реакт непосредственно и с хуками Реакта. Ну а так да, вам не нужны тут эффекты, и так хорошо код выглядит
Effect же это про программирование на эффектах, есть система эффектов, рантайм и тп.
А вы не думали, что раз на хабре нету статьи про него, и вы первый, значит он и не нужен?)
Лучше не библиотеки учить, а идеи. Например, научить команду работать с ошибками и с исключениями отдельно. Понимать что есть ошибка (нормальный флоу, просто негативный), и исключение (исключительный флоу, когда все пошло не так)
Effect для TypeScript разработчиков