Сайд эффекты что это

[thoughts] Сайд-эффекты: можно ли лучше чем redux-saga и ngrx/effects?

Я некоторое время работал с redux-saga и ngrx/effects. От этого у меня слегка припекло и я начал раздумывать о более простой модели сайд-эффектов.

Критиковать эти инструменты — утомительно. Если вкратце, то они приносят больше сложности, чем необходимо для решения задач. Но все же, я попробую критику превратить в “чего хочется”.

У redux-saga одна из проблем заключается в том, что там слишком легко пропустить событие (см. “Non-blocking calls”). Другая проблема — использование генераторов, что порождает трудности дебага и типизации. С другой стороны, redux-saga позволяет описать понятным образом сайд-эффекты (если не обращать внимание на нейминг).

Проблему ngrx/effects сформулировать трудно. Если быть точнее, то проблема заключается в сложности модели rxjs, заточенной под все-подряд. Сходите на документацию ngrx/effects, проскролльте до конца и убедитесь сами в том, что оно ебанутое. С другой стороны, фишки вроде возможности сделать filter, map, debounce на потоке событий — крутая штука.

Чутка терминологии

Дальше в статье сайд-эффекты будут упоминаться только в такой трактовке:

Сайд-эффект — это логика, которая делает что-то в ответ на изменение модели (стора) или в ответ на событие, связаное с моделью (стором).

Любые иные сайд-эффекты рассматриваться не будут — ими пускай занимается соответствующий слой.

Дисклеймер

Сразу предупрежу, что если вам нужны очень примитивные сайд-эффекты, то вы пишете небольшой сахарок поверх чего угодно и радуетесь жизни. А статья о том, чтобы можно было писать и вещи посложнее.

Чего хочется

  • Я хочу легко диспатчить ноль/один/бесконечно экшенов в ответ на случившийся экшен (ngrx/effects плох)
  • Я хочу знать легко знать стейт до экшена и стейт после экшена (ngrx/effects плох)
  • Я хочу легко дебажить и понятные стектрейсы (ngrx/effects плох)
  • Я не хочу ощущать себя ракетостроителем (redux-saga и ngrx/effects плохи)
  • Я хочу понятный код, совместимый с основными концептами typescript (redux-saga и ngrx/effects плохи)
  • Я хочу, чтобы пропустить событие было трудно (redux-saga плох)
  • Я хочу иметь гипотетическую возможность реагировать на последовательность экшенов (ngrx/effects плох)
  • Я хочу, чтобы простые сайд-эффекты описывались простым кодом, а сложные — соизмеримым по сложности (ngrx/effects плох)

Истоки проблем

Почти вся история развития redux-saga, rxjs и прочего говна основана на том, что в языках программирования нету средств для удобной работы со стримами.

Соответственно, люди придумывают различные костыли для работы с ними. Подобную историю вы можете вспомнить насчет концепции Promise/Task. Одни ребята улучшали убожество промисов добавлением новых методов на все случаи жизни. Другие ребята пытались с помощью yield приделать императивный вид асинхронному коду. А потом разработчики языков проснулись и убрали нужду в костылях.

А разве у нас реально сейчас есть нужда в костылях?

Улучшаем redux-saga

Итак, для начала просто. Нам нужно убрать к черту генераторы. По-сути, нужда в генераторах не так сильна, если убрать часть фич (которые мне не нужны).

Для начала ограничимся функциями take, select, put, call, fork. А функционал отмены тасков и коммуникацию через каналы — уберем.

Мы можем с легкостью заменить все вызовы yield smth(. ) на вызовы await smth(. ) или даже smth(…) . Но, нужно будет передавать эти функции внутрь описываемого эффекта (чтобы была привязка к стору).

Так, например, функция на redux-saga:

Превратится в такую функцию:

Я переименовал take -> waitFor , select -> getState для большей ясности.

Как должна работать функция waitFor? Мы где-то будем хранить буффер всех произошедших экшенов, но еще необработанных экшенов. Вызов waitFor будет выдавать следующий произошедший экшен и убирать его из буффера. Если экшенов нет — будет ждать пока появятся. Придется произвести такие же манипуляции с getState для консистентности. Прим.: оригинальная функция take работает не так — она не хранит буффер, просто ждет новый таск.

Нужда в функции call (и apply) пропадает — сейчас можно просто вызвать функцию и не капать себе на мозг.

Функция fork будет создавать для подсаги независимый буффер экшенов/стейта — чтобы независимые саги не конкурировали за события.

Вообще, строго говоря, функция fork не очень-то и нужна. Подсаги можно описывать и регистрировать независимо, не пытаясь их скомбинировать в root-сагу. Конечно, пропадает куча интересных возможностей.

При наличии функции fork, реализовать функции takeEvery, takeLatest, takeLeading является тривиальной задачей (это всего-лишь сахар). Без fork уже нетривиально, но все еще реально.

Функционал throttle и debounce тоже можно реализовать, но есть риск, что сильно изменится синтаксис.

Улучшаем ngrx/effects

Мое предложение такое: а давайте больше не будем выебываться, и будем использовать ныне нативный AsyncIterable вместо Observable.

Для начала, мы можем написать конвертер Observable → AsyncIterable. Этот конвертер должен буфферизировать все поступающие события и выдавать их по требованию.

Также вместо потока “экшенов” мы будем работать с потоком “экшен + + новое состояние”. Можно еще добавить даже предыдущие состояния, но это чтобы совсем все засахарилось.

Это даст нам возможность писать следующий код:

Окей, что насчет filter/map? Ну гипотетически, мы можем их добавить к нашему AsyncIterable, как и остальные необходимые нам методы. Ну это если нам очень нужно: ведь внутри сайд-эффектов нет большой необходимости в этом.

Либо еще проще: передавать внутрь сайд-эффекта Observable, а сайд-эффект сам пускай конвертирует его в AsyncIterable, предварительно вызывая любые необходимые операторы типа filter/map.

Гипотетически мы можем написать функцию pipe, которая приводит AsyncIterable к Observable, применяет оператор, и конвертирует обратно. Ну так, чисто в порядке бреда.

Если мы хочем дожидаться цепочку ивентов, то нам нужно, чтобы events были не просто AsyncIterable, а AsyncIterableIterator. Тогда мы сможем написать такой код:

Мы можем пойти чуть дальше, и сделать так, чтобы нашему потоку событий нельзя было сказать “горшочек, не вари”. Для этого придется убедиться, что функция return у нашего AsyncIterator делает ничего (или отсутствует). Тогда мы сможем использовать несколько for await.

В частности, это сделает возможным реализацию подобного кода (это переделанный пример из документации redux-saga):

И да, в подобной модели придется явно диспатчить экшены, но это к лучшему.

Что такое side эффект?

maxfarseer

Sideeffect — это что-то, что может повлиять на «чистоту» вашей функции. Редьюсер же — функция. Чистая функция, это значит такая, что если ей на вход подать одни и те же параметры, то результат будет всегда один и тот же.

Пример: есть у вас в localStorage имя пользователя. И вы в коде пишите, что-нибудь такое:

Следовательно, если вы подадите на вход функции, имя Вася, то оно вам вернет Васю только если «в sideeffect локал_сторадже» нет ничего. Здесь вы не можете быть уверены, что если подать Васю, вам всегда вернется Вася.

По примеру с комментариями — не думаю что хороший пример. Айдишники генерировать будет бэкэнд ваш. Вы добавляете новый комментарий путем отправки его на сервер, с сервера приходит статус «ОК» и ваш комментарий уже с айдишником.

Бывает, что айдишники нужно генерировать самому, тогда они отлично генерятся в acftionCreator’ax. Например, делаете вы систему уведомлений, и у каждого уведомления должен быть свой id (например, тут сервер вам не нужен, вы ничего туда не отправляете, просто визуальная часть). В таком случае, я бы не стал генерировать id через middleware, а просто делал бы это в «экшенах».

Тем не менее, с генерацией айди все тоже самое, что и с localStorage. Вы не уверены, что подав на вход: имя, текст комментария и почту — получите ТОТ же результат, что и в прошлый раз с такими же входными параметрами (айдишники то разные будут!)

Чистые функции — JS: Функции

Функции в программировании обладают рядом важных характеристик. Зная их, мы можем точнее определять, как лучше разбивать код на функции и когда вообще их стоит выделять.

Детерминированность

Встроенная в JavaScript функция Math.random() возвращает случайное число от 0 до 1:

Функция нужная и полезная, но неудобная в отладке и тестировании. Это связано с тем, что для одних и тех же входных аргументов (отсутствие аргументов также попадает под это понятие), она может возвращать разные значения. Функции с таким поведением называются недетерминированными.

Например, недетерминированными являются функции, оперирующие системным временем. Так, функция Date.now() каждый раз возвращает новое значение:

А вот пример с аргументами. Представьте функцию getAge() , которая принимает на вход год рождения и возвращает возраст:

Хотя прямо сейчас повторный запуск вернёт точно такое же значение, через год оно уже будет другим. То есть функция считается недетерминированной, если она ведёт себя так хотя бы единожды.

Детерминированные функции, напротив, ведут себя предсказуемо. Для одних и тех же входных данных они всегда выдают один и тот же результат. Именно такими являются функции в математике.

Интересно что, например, функция console.log() — детерминированная. Дело в том, что она всегда возвращает одно и то же значение для любых входных данных. Это значение undefined , а не то, что печатается на экран, как можно было бы подумать. Печать на экран — побочный эффект, о нём мы поговорим чуть позже.

Вызов console.log('Hexlet — Big Bang') выполнил два действия:

  • Вывел сообщение Hexlet — Big Bang в терминал (или консоль браузера, в зависимости от среды выполнения)
  • Вернул значение undefined . Какое сообщение бы мы ни печатали, возвращаемое значение всегда будет одно — undefined

Функция становится недетерминированной и в том случае, если она обращается не только к своим аргументам, но и некоторым внешним данным, например глобальным переменным, переменным окружения и так далее. Так происходит потому, что внешние данные могут измениться, и функция начнёт выдавать другой результат, даже если в неё передаются одни и те же аргументы.

Функция getCurrentShell() обращается к переменной окружения SHELL . Но в разные моменты времени и в разных окружениях значение этой переменной может быть различным.

В общем случае нельзя сказать, что отсутствие детерминированности — абсолютное зло. Для работы многих программ и сайтов нужна функция, возвращающая случайное число или вычисляющая текущую дату. С другой стороны, в нашей власти разделить код так, чтобы в нем было как можно больше детерминированных частей. Общая рекомендация при работе с детерминированностью звучит следующим образом: если есть возможность написать функцию так, что она будет детерминированной, то так и делайте. Не используйте глобальных переменных, создавайте функции, зависящие только от своих собственных аргументов.

Понятие "Детерминированность" не ограничивается программированием или математикой. Сквозь него можно рассматривать практически любой процесс. Например, подбрасывание монетки — недетерминированный процесс, его результат случаен.

Побочные эффекты (side effects)

Вторая ключевая характеристика функций — наличие побочных эффектов. Побочными эффектами называют любые взаимодействия с внешней средой. К ним относятся файловые операции, такие как запись в файл, чтение файла, отправка или приём данных по сети и даже вывод в консоль.

Кроме того, побочными эффектами считаются изменения внешних переменных (например, глобальных) и входных параметров в случае, когда они передаются по ссылке.

А вот вычисления (логика), напротив, не содержат побочных эффектов. Например, функция, суммирующая два переданных аргументами числа.

Побочные эффекты составляют одну из самых больших сложностей при разработке. Их наличие значительно затрудняет логику кода и тестирование. Приводит к возникновению огромного числа ошибок. Только при работе с файлами количество возможных ошибок измеряется сотней: начиная с того, что закончилось место на диске, заканчивая попыткой читать данные из несуществующего файла. Для их предотвращения код обрастает большим числом проверок и защитных механизмов.

Без побочных эффектов невозможно написать ни одной полезной программы. Какие бы важные вычисления она ни делала, их результат должен быть как-то продемонстрирован. В самом простом случае его нужно вывести на экран, что автоматически приводит нас к побочным эффектам:

В реальных же приложениях, обычно, все сводится к взаимодействию с базой данных или отправкой запросов по сети.

Не существует способа избавиться от побочных эффектов совсем, но их влияние на программу можно минимизировать. Как правило, в типичной программе побочных эффектов не так много по отношению к остальному коду, и происходят они лишь в самом начале и в конце. Например, программа, которая конвертирует файл из текстового формата в PDF, в идеале выполняет ровно два побочных эффекта:

  1. Читает файл в самом начале работы программы.
  2. Записывает результат работы программы в новый файл.

Между этими двумя пунктами и происходит основная работа, которая содержит чистую алгоритмическую часть. Побочные эффекты в таком случае будут находиться только в верхнем слое приложения, а ядро, выполняющее основную работу, останется чистым от них.

Инкремент и декремент — единственные базовые арифметические операции в JS, которые обладают побочными эффектами (изменяют само значение в переменной). Именно поэтому с ними сложно работать в составных выражениях. Они могут приводить к таким сложноотлавливаемым ошибкам, что во многих языках вообще отказались от их введения (в Ruby и Python их нет). В JS стандарты кодирования предписывают их не использовать.

Чистые функции

Идеальная функция с точки зрения удобства работы с ней называется чистой (pure). Чистая функция — это детерминированная функция, которая не производит побочных эффектов. Такая функция зависит только от своих входных аргументов и всегда ведёт себя предсказуемо.

Что такое side-effects

что такое side-effects и с чем их едят? В каких коварных моментах языка стоит помнить про данный термин. Когда незнания его может привести к последствиям.

По анализу вопросу понял, что это: если функция не умеет внешних связей и работатет по принципу:
вы мне запрос — я вам ответ.
То такая функция без side-effect’ов.

Что такое файловый буфер? Что такое режим (модификатор) доступа, при работе с файлами?
Что такое файловый буфер? Что такое режим (модификатор) доступа, при работе с файлами?

Что такое рекурсивный тип данных? Что такое конструкция рекурсивного типа?
Что такое рекурсивный тип данных? Что такое конструкция рекурсивного типа?

Что такое хэндлер файла? Что такое файловый указатель?
Что такое хэндлер файла? Что такое файловый указатель?

Что такое заголовочный файл? Что такое файл исходного кода? Рассмотрите назначение каждого из них
Что такое заголовочный файл? Что такое файл исходного кода? Рассмотрите назначение каждого из.

Сообщение от rikimaru2013
Лучший ответСообщение было отмечено rikimaru2013 как решение

Решение

rikimaru2013, хочешь более нетривиальных примеров?

то, если a++ — true, то `b` инкрементируется, в противном случае — нет. Если b++ — true, то `с` тоже инкрементируется, в противном случае нет. Если все выражение — true, то входим в блок if. Совместно с проверкой осуществляются некоторые действия, которые зависят от ленивости оператора && и каким-то образом в итоге влияют на логику программы.
А теперь представим, что у нас появилась перегрузка оператора && для некоторых классов, а операторы ++ примененные к `a`,`b`, `c` возвращает объекты этих классов.
Появляются другие сайдэффекты:
1) Пропадает ленивость.
2) Порядок вычисления аргументов теперь не определен.

Резюмируя: никогда не полагайся на ленивость оператора && (и подобных с тем же свойством), если существует ненулевая вероятность, что в данном контексте он может быть перегружен. Ленивостью обладает только встроенная версия, а пользовательская перегрузка обладает только свойствами обычной функции.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *