Мы еще сделаем из вас Монаду

В частности, давайте превратим собственный массив Javascript в монаду! Это будет легко, так как массивы уже почти всегда там!

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

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

Является ли массив «заостренным»? Сорта. Сказать, что что-то «заостренное» достаточно просто: это просто означает, что есть способ поднять ценность в чудесный мир массивов. Помещать значения в массив довольно просто: вы можете буквально просто записать его в буквальном виде, верно?

[9] Я поместил 9 в массив! Выполнено.

… Ну, это не совсем то, что мы имеем в виду: нам нужен функциональный способ сделать это: функция для переноса значений, специфичная для массивов. Может работать такая функция: x = ›[x].

Потому что с этим мы можем делать что-то вроде: compose (xs = ›[0,… xs], x =› [x]);

То есть, если у нас есть значение для начала, но мы хотим использовать функцию, которая ожидает массив, мы можем обязательно «обновить» любое значение в массиве: x = ›[x] операция просто поместит значение в массив, так что следующий шаг, xs = ›[0,… xs], будет иметь массив работать, как ожидается:

compose (xs = ›[0,… xs], x =› [x]) (5); // - ›[0,5]

Вы можете понять, почему заостренный интерфейс может пригодиться, когда вы объединяете функции, которые ожидают разные типы, верно?

Что ж, с ES2015 у нас даже есть собственная версия «заостренных» для массивов (вроде как). Он называется Array.of. Пока мы даем ему только одно значение, его должно хватить для наших целей: Array.of (6) → [6]. Отлично.

Пока что нам не пришлось делать никакой реальной работы. Но в массивах фактически отсутствует последний момент: способ «сгладить» в стиле Functor. Пока нас не волнует опасность расширения нативных прототипов, это довольно легко реализовать:

. flatten () возьмет массив массивов и раскроет его на одном уровне, все благодаря как аккуратному оператору распространения ES2015, так и почти волшебному поведению Array.prototype.concat (). Мы теперь монада?

Ну откуда нам знать? Мы бы знали, потому что, конечно же, Array будет подчиняться законам монад! Не беспокойтесь, если вы еще не знакомы с ними: нам не нужно понимать их, если все, что мы хотим сделать, это проверить, проходит ли наш измененный тип Array. Вот небольшой набор тестов, который я приготовил ... но пока, пожалуйста, просмотрите его, не обращайте внимания на комментарии и сразу переходите к кульминации в конце:

Изюминка? Этот небольшой набор тестов неоднократно вызывает метод .chain,, который мы с вами оба должны быть уверены, что у массивов нет. Итак, наличие метода .flatten () (иногда называемого .join ()) не является Это не совсем то, что мы имели в виду под словом «может сглаживать». Сглаживание здесь - это не просто сдавливание массива: это операция, которая применяет определенную функцию к к каждому значению (ям) внутри массива, например .map (). Фактически, это в основном ЕСТЬ просто .map () плюс смех:

Некоторые библиотеки даже называют такую ​​операцию . flatMap () (почему бы вместо этого не назвать ее .mapFlat () , так как все происходит в таком порядке? Я не знаю). Как оказалось, оба Array .flatten и Array .flatMap ведут переговоры о том, чтобы стать официальной частью Javascript .

Однако послушайте мой совет: не просто думайте о .chain как о удобном сокращении для .map (). flatten (). В некотором смысле это даже более фундаментально, чем .map, если вы можете в это поверить. Это потому, что, как только мы определим допустимую операцию. chain для некоторого монадического типа (а также заостренный метод), мы всегда можем легко и надежно получить .map реализация от них. Создав сначала версию Array.prototype.chain () с нуля без использования .map () или .flatten () я могу даже привести вам пример; Я называю это… map2:

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

Важно понять, почему .chain удаляет только один уровень и один уровень точно. Сопоставление обычно включает функцию, которая превращает некоторое значение a в какое-то новое значение b: a → b

Но если задуматься, существует множество функций, которые обязательно принимают значение и вместо этого возвращают тип контейнера, например Array, со значением внутри. Array. of - это простой, с которым мы уже сталкивались: a → [a]. И если вы работали с ES6 Promises, вы, вероятно, создали множество функций с такой сигнатурой типа: a → Promise [b] (например, функция, которая принимает маршрут URL-адреса api в виде строки и возвращает обещание для результата json) Та же сделка.

Но подумайте, что произойдет, если мы передадим функцию с этой разновидностью сигнатуры типа, например a → F [b], просто в F .map () (F - это просто общий тип контейнера, который можно представить как массив с одним элементом). Что ж, в итоге мы получим вот что:

Функтор F :: F [a] → (a → F [b]) → F [F [b]]

Так что это раздражает. Но, автоматически удаляя только один внешний слой, мы можем вместо этого получить новую сигнатуру типа:

Монада M :: M [a] → (a → M [b]) → M [b]

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

Это может показаться очень непонятным случаем, но подумайте, что это означает: даже если типы входов и выходов функции, подобной a → M [b], обязательно различны (т. Е. Несимметричны) , входы и выходы полной операции .chain (a → M [b])… одинаковы. Это означает ... что теперь все эти операции можно снова аккуратно скомпоновать, вывод одного соединяется прямо с вводом другого без какой-либо дополнительной умственной гимнастики.

Хорошо, надеюсь, все это наводит на размышления и интригует, но зачем вам вообще нужны .chain () массивы, конкретно? Разумеется, в этом и заключается вся тавтологическая цель того, чем мы здесь занимаемся: добавление этой возможности к массивам делает их легитимными монадами. (Теперь Array пройдет все эти тесты. Вы можете снова просмотреть их. Или проверить их в реальном REPL.)

Но это не очень хороший ответ, тем более что я никогда толком не объяснял, что такое монада (и, если вы уже знаете, скорее всего, вы не удосужились дочитать до этого места). Итак, опять же, зачем определять .chain () и делать собственные массивы монадическими?

Я допускаю, что по сравнению с другими монадами, собственные массивы javascript не обязательно очень интересны. Во-первых, мы уже воспринимаем большую часть их функций как должное. И они не инкапсулируют какие-либо интересные дополнительные поведения, такие как лень, неизменяемость, отдельные подтипы и т. Д. Но они обладают той гибкостью, которую вы ожидаете от монад в целом, некоторые из которых мы скоро посмотреть на практике. И хотя первая монада, представленная во многих руководствах, представляет собой что-то вроде Identity, Array-as-Monad по крайней мере имеет имеет несколько более очевидных интересных вариантов использования прямо вне ворот (которые Типа идентичности, в основном, нет).

Одна вещь, которую мы можем довольно легко сделать сейчас, - это вкрапление значений в массив без какого-либо императивного цикла:

[1,2,3,4] .chain (x = ›[x, 0]). slice (0, -1); // -› [1 , 0, 2, 0, 3, 0, 4]

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

[1,2,3] .map (x = ›[x + 1, x + 2, x + 3]); // -› [Array [3 ], Массив [3], массив [3]]

… Но мы действительно хотим, как и во многих распространенных случаях использования, полный, поверхностный список всех возможных перестановок для всех значений. И снова .chain сделает именно это за нас:

[1,2,3] .chain (x = ›[x + 1, x + 2, x + 3]); // -› [2, ​​3 , 4, 3, 4, 5, 4, 5, 6]

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

Array.of (x = ›x + 1, x =› x + 2); // - › [функция, функция]

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

Но что теперь? Чтобы на самом деле использовать эти функции, нам понадобится способ передать некоторый массив значений (которые станут аргументами) в каждую функцию, чтобы они могли возвращать полные и исчерпывающий массив результатов. Что ж, когда мы определили Array как монаду и получили метод .chain, мы можем очень легко получить другой метод: .ap:

.ap, сокращение от Applicative / Apply (но не в точности то же самое, что Function.prototype.apply), позволяет нам принимать значение (s) в нашей монаде массива (значения, которые оказываются функциями) и, сопоставив второй массив, который мы передали этому новому методу .ap, запустите каждую функцию с каждым значением во втором массиве.

Это означает, что мы можем просто использовать интерфейс .ap, который мы создали для любого целевого массива, и получить полный результат (2 функции x 3 значения = 6 результатов):

Array.of (x = ›x + 1, x =› x + 2) .ap ([1,2,3]); // - ›[2 , 3, 4, 3, 4, 5]

Примеры использования для этого?

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

var curveOne = x = ›[x, (x + 1) * (x + 2)];
var curveTwo = x =› [x, (x + 3)];

[curveOne, curveTwo] .ap ([1,2,3,4,5]);

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

Конечно, это действительно простой пример, но когда все начинает усложняться, важно иметь простой и гибкий интерфейс. Допустим, все наши мутационные функции принимают более одного входа, потому что сексуальная рекомбинация - это вечеринка. Может ли прикладной интерфейс для массивов справиться с этим? Конечно. Пока наши функции каррированы (т.е. не будут выполняться, пока не получат оба своих аргумента):

var mOne = curry ((x, y) = ›x - y);
var mTwo = curry ((x, y) =› x + y);
var mThree = карри ((x, y) = ›x * y);

Теперь мы собираемся пропустить два набора beasties через все возможные перестановки, просто применив первый массив beasties, а затем применив второй массив beastie mates:

Array.of (mOne, mTwo, mThree) .ap ([1,2,3]). ap ([10,11,12]);
// - ›[-9, -10, -11, -8, -9, -10, -7, -8, -9, 11, 12, 13, 12, 13, 14, 13, 14, 15 , 10, 11, 12, 20, 22, 24, 30, 33, 36]

Таким образом, каждый зверь из первого массива сопоставляется с каждым зверьком из второго массива, прошедшего через каждую перестановку, которую мы изначально перечислили (сам по себе просто массив) . В результате получилось 27 новых чудовищ (3 x 3 x 3). Под капотом у нас есть только 3 каррированные функции в массиве. Каждый из них получает один из своих аргументов, применяемых в каждом из 3 значений из первого массива, а затем каждый из них получает свой второй аргумент, применяемый из каждого значения второго массива. Таким образом завершенные, они возвращают результат: сплющенный массив.

Это очень гибкое и понятное представление того, что может очень легко вызвать у вас головную боль, если вы попытаетесь написать это императивно с помощью циклов и циклов в циклах (если вы интуитивно догадываетесь, что эти конкретные операции также могут быть выполнены с помощью преобразователей) , Золотая Звезда). Это своего рода устрашающее "излишнее сочувствие!" Проблема, которую вы обычно пытаетесь избежать из-за всей потенциальной головной боли, но здесь решается аккуратно и легко. Хорошо сконструированный тип может позаботиться обо всех сложных вещах за вас.

Как бы то ни было, это основной метод A pplicative,. ap (), который очень эффективно используется с массивами (позволяя нам сортировать обратные операции, которые мы обычно используется с .chain ()). Как насчет еще нескольких вариантов использования .chain () напрямую?

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

var childrenOf = DOMnode = ›Array.from (DOMnode.childNodes);

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

Давайте сначала создадим удобную версию document.getElementsByTagName (которая всегда возвращает массив, даже если он находит один или нет узлов):

var $ byTag = tag = ›Array.from (document.getElementsByTagName (tag));

Благодаря этому у нас есть функция, которая возвращает массив узлов DOM (также настоящий массив: спасибо Array. from!), что означает, что мы можем просто .chain () в нашей асимметричной функции поиска дочерних узлов, childrenOf. И мы даже можем сделать это дважды, как рекламируется:

$ byTag (‘body’). chain (childrenOf) .chain (childrenOf);

Это возвращает всех дочерних элементов из дочерних элементов из тега body! Очень просто. Как насчет того, чтобы мы хотели определить именованную функцию, которая все это делает, вместо явного связывания? Давайте создадим приятный «безточечный» помощник, который принимает функцию и монаду, а затем делегирует методу .chain любую монаду, которую мы передаем (без каких-либо предположений). :

var chain = curry ((f, xs) = ›xs.chain (f));

Теперь мы можем написать:

var grandchildrenOf = compose (chain (childrenOf), chain (childrenOf));

А затем используйте это как:

grandchildrenOf ($ byTag (‘body’)); // - ›[Boatload, O’, DOMnodes]

Монада входит, монада выходит. То есть: массивы на всем протяжении, пока мы вычисляем наш (массив) результат. И для этого мы в основном взяли простой, но асимметричный, childrenOf и сделали его так, чтобы операция in-toto была симметричный. Мы можем .chain () использовать столько функций childrenOf, сколько захотим, чтобы получить желаемую глубину и все работает: без чрезмерного усложнения childrenOf.

Между .map (), .ap (), и .chain () теперь у нас есть более полный своего рода набор инструментов в стиле Honeybadger. Мы можем выполнять разумные вычисления над значениями внутри наших монадических контейнеров (здесь массивы, но они всего лишь верхушка айсберга) независимо от того, являются ли эти вычисления той сортировкой, которая возвращает одиночные значения или сами массивы. Свобода!

Такие библиотеки, как jQuery, справляются с подобной магией под капотом (хотя фразы вроде «jQuery - это монада» обычно больше сбивают с толку, чем разъясняют). Но когда jQuery вышел из моды, стоит узнать, как работает магия напрямую.

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

Это потому, что существуют монадические паттерны, которые вам уже удобно использовать и которые вы интуитивно понимаете. Мы только что увидели, насколько незначительное изменение требуется, чтобы сделать Array монадой, и почему это может быть полезно.

Я также утверждаю, что обещания ES2015 - это тоже монады, прямо из коробки.

Конечно: они странным образом кипятят .map () и .flatMap () / .chain () в ту же чрезмерно умную операцию: .then () Теоретически это может немного сбивать с толку и нарушает некоторые духа функционального программирования «стандартизованное поведение = универсальные операции», но в большинстве случаев это очень естественно работает на практике.

Их заостренная функция находится в Promise.resolve вместо Promise.of, но это также должно иметь смысл: вы поднимаете известное значение в Promise, и, поскольку это просто значение, а не ошибка, имеет смысл, что оно находится в разрешенном состоянии. (Зачем вообще нужно преобразовывать простое значение в Promise? Ну…) Если хотите, вы всегда можете просто присвоить ему псевдоним: Promise.of = x = ›Promise.resolve (x)

Также стоит признать, что Promises, строго говоря, не являются чистыми (когда вы создаете его, его оценка ввода-вывода выполняется немедленно / с нетерпением, а не с ожиданием вызова, и большинство функций, возвращающих Promise, зависят от некоторого внешнего состояния, например api) . Но, несмотря на все эти предостережения, они все же подчиняются основным монадическим законам: http://goo.gl/tCQ3c4

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

Вы знаете, какие типы функций могут обрабатывать .then (), например: унарные функции, которые возвращают либо значения (потому что это функтор и может отображать) или новые промисы напрямую (flatMapping). Но вы также точно знаете, что, поскольку монады по своей сути являются аппликативами, можно написать аппликативный интерфейс для обещаний, используя .then ().. Можно даже представить интересный вариант использования:

Функция внутри обещания вместо значения? Готов поспорить, вы даже не думали, что обещания можно использовать таким образом (представьте себе обещание, которое возвращает нормализованный интерфейс функции для одного из двух возможных API, определенных во время выполнения…)! Но действительно ли это лучше, чем делать это вместо этого?

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

Но вот еще одно важное осознание: Promise.all и наш оригинальный метод Array.ap на самом деле имеют некоторые довольно важные связи. . Я, как и многие энтузиасты ES6, возможно, принял что-то вроде Promise.all как должное: некую загадочную функцию, объединяющую обещания (родная замена для jQuery $ .when). Но давайте подумаем подробнее.

Какая основная сигнатура Promise.all? Конечно, он принимает массив и возвращает обещание, но более конкретно: он принимает массив обещаний и возвращает обещание… массивов. Итак, это сложный тип, обернутый вокруг другого сложного типа, но затем с этой оберткой, вывернутой наизнанку! Как это достигается? В случае Promise.all это своего рода черный ящик, специфичный для массивов и промисов.

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

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

Гибкость сама по себе может быть кошмаром (слишком много способов сделать одно и то же), но гибкость здесь ограничена очень специфическим набором хорошо известных законов и свойств. Вот почему это круто. 👲