Как дизайн JS Promise пересекается с абстракцией Either
Сегодня, спустя несколько лет после того, как Promises захватили нашу повседневную жизнь с JavaScript, мы оглядываемся назад и пропускаем старые добрые дни обратного вызова.
Помните, как легко было просто подключиться к базе данных, а затем прочитать что-то из нее только для того, чтобы произвести несколько вычислений и сохранить это обратно? Эти три уровня отступа в вашем последнем бите обработки псевдо-ошибок. Те лояльные утверждения if, проверяющие истинность переменной торжественной ошибки, только она не знает точно, что делать, когда неизменно приходит с плохими новостями.
Да, по тем дням никто не скучает. Обратные вызовы хороши в качестве низкоуровневой конструкции, но когда вы хотите эффективно и безопасно координировать многие из них, вы, скорее всего, окажетесь в плохом месте, о котором вам все говорили.
Введите обещания
Да, они были у нас в жизни JS задолго до официального включения в наш любимый ECMAScript 2015. Такие библиотеки, как bluebird и q, уже реализовали свои версии этой удивительной абстракции, которая навсегда изменит нашу жизнь, и все, кто был в курсе, использовали их до того, как это стало круто.
В настоящее время обещания распространены повсеместно, и мы боимся минут, которые тратим на написание обратных вызовов для этого устаревшего кода, который мы должны поддерживать раз в неделю, или даже на использование этой библиотеки-отступника с синдромом Питера Пэна. (Даже setTimeout
кажется мне странным, если честно.)
Все мы знаем, как выглядят обещания в реальных условиях, но что мне особенно интересно, так это их сходство с более формальным типом Either
. Если вы знаете себя Haskell, вы, скорее всего, знакомы с ним. Для ацианцев Rust он присутствует всегда, но под другим именем. Если вы используете Elixir, вы, вероятно, привыкли к его разновидности, используя кортежи, атомы и некоторый синтаксис. Но если вы напишете какой-нибудь JavaScript, вы живете и дышите Либо, даже не зная.
Либо… что?
Either - это просто тип, который представляет одно из двух. Вот и все. Это может быть int
или string
. Это может быть яблоко или подписанная копия вашего любимого альбома. Это могло быть Either
что-то или другое что-то.
Если это кажется слишком упрощенным, это потому, что это так.
Формальное определение, которое вы, скорее всего, увидите, похоже на Either a b = Left a | Right b
. a
и b
могут быть любыми, это параметры. |
означает либо одно, либо другое. Это либо Left
со значением типа a
, либо Right
со значением типа b
. Может быть и то, и другое? Не может ли быть ни того, ни другого? Может быть Left
с b
? Right
с a
? Нет. (:
Хорошо, либо тогда. И что?
Вы могли догадаться, что это определение очень хорошо соответствует Promise
семантике. У него есть два вида результатов, два конвейера обработки, и он попадает только в один из них. Никогда ни разу и ни разу и то и другое.
В этом смысле функции resolve
и reject
в модуле Promise
являются простыми конструкторами. Можем ли мы назвать их… Left
и Right
?
Я знаю я знаю. Это довольно слабая параллель, и она не имеет большого значения в одиночку, но потерпите меня, есть еще кое-что.
"карта" не только для списков
Either
работает как функторы, что означает, что вы можете применить преобразование к базовым данным, не затрагивая содержащую структуру. Другими словами, вы можете map
значений в другие значения, не теряя Either
контейнер.
Поскольку вы не знаете, какое из двух возможных значений имеет Either
, у вас обычно есть две функции отображения: одна работает с левым значением, а другая - с правым. К счастью, в этом случае могут помочь общие имена: mapLeft / mapRight.
Как это соотносится с JS Promise? Когда все идет хорошо, вы можете связать преобразования с данными, вызывающими then
. Когда что-то пойдет не так, вы можете исправить ошибку, связав catch
вызовов.
Теперь, если вы следите за мной по этому поводу, вы можете подумать: вы можете сопоставить с другими обещаниями в JS. Что тогда происходит с Either
?
Поскольку вы привыкли к обещаниям, вы знаете, что не попадаете в подобные ситуации. Вложенные обещания - это не вещь, вам не нужно беспокоиться о них обоих. Они слились в одно целое, правда?
"flatMap" не только для списков
Оказывается, это монадическое свойство, и его также можно найти в типе Either
. Есть функция, которая также отображает значения, но вместо того, чтобы отображать их внутри контейнера, она позволит вам сопоставить с другим контейнером и объединит эти два контейнера в один.
В Either
терминах вы можете отобразить значение в другое Either
. Функция оставит вам только один Either
, полученный в результате комбинации обоих. В Promise
терминах вы можете сопоставить значение другому Promise
, и у вас останется одно Promise
, которое объединяет их оба.
Эта (очень мощная) функция обычно называется flatMap
или andThen
. В JS это называется then
(или catch
). Та же функция, которая работает как mapRight
, работает как flatMap
, в зависимости от того, что вы возвращаете. Таким образом, семантика приведенного выше кода на самом деле будет такой:
Почему тогда он не называется Either вместо Promise?
Справедливый вопрос. Я утверждаю, что семантика Promise
, которую мы видим в текущем JavaScript, во многом заимствует семантику абстракции Either
. Однако они не совсем то же самое, особенно из-за простого упущенного до сих пор факта: Either
не имеет ничего общего с асинхронными рабочими процессами, и это все, о чем JS Promise
.
Они пришли и забрали землю, которой когда-то правили колбэки. Синхронные рабочие процессы просты, и для этого нам не нужно было никаких обещаний. Они пришли помочь с трудным моментом - асинхронными рабочими процессами!
Да все верно. Дело в том, что Promise
- тоже известная абстракция. Это просто не так сложно, как реализация JS. Promise
(в общем) - это простая абстракция, представляющая отложенное значение. В своей простейшей форме он не поддерживает отображение, вычисление цепочки или даже обработку ошибок. Это просто ценность, которая когда-нибудь будет там, но не сейчас. Кому нужна ценность, придется ее дождаться.
Ожидание обычно означает разыменование или блокировку потока до тех пор, пока значение не будет готово. В современном JS у нас есть удобная функция разыменования, закодированная в ключевое слово, которое нравится многим: await
.
A JS Promise
- это, по сути,Promise
.
Дело в том, что это не просто Promise
, это нечто большее. Это почти похоже на Promise
, который переходит в Either
. Или Either
с Promise
на каждом конце.
JS обещает рок!
В общем, мне очень нравится JS Promise
абстракция и реализация, и не только потому, что все были травмированы работой с обратными вызовами.
Эта концепция использует упрощенный и мощный асинхронный примитив Promise
, добавляет некоторые механизмы обработки ошибок и приправляет его мощными функциями сопоставления, которые можно связать вместе. Затем он упаковывается с смехотворно простым API, и готово.
В итоге мы получаем гораздо более чистый код, который значительно упрощает чтение асинхронных вычислений, а также упрощает управление ошибками и их труднее игнорировать.
Это стильно и точно.