Как дизайн 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, и готово.

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

Это стильно и точно.