JS ФП тезисы: различия между версиями
imported>Supportadmin Нет описания правки |
imported>Supportadmin |
||
(не показано 99 промежуточных версий этого же участника) | |||
Строка 1: | Строка 1: | ||
==Вместо вступления== | |||
*[http:// | *[http://s.arboreus.com/2009/09/haskell-horrors.html] | ||
*[https:// | *[http://shamansir-ru.tumblr.com/post/18189187384/the-asynchronous-samurai-way] | ||
Для человека, привыкшему к императивным языкам, испорченному годами объектно-ориентированного мышления монады кажутся странными. Монады являются и одновременно не являются контейнерами. Они — обёртки, упаковки для вычислений, а не для значений. Однако они обёртывают вычисления не для того, чтобы их было удобнее хранить в монадных коробочках, а чтобы их можно было удобнее соединять друг с другом. Вместо «коробочек» представьте обыкновенные кирпичи, которые ровно кладутся друг к другу. Это, кстати, похоже на шаблон Adapter в ОО-проектировании. Каждая монада определяет какой-то способ передавать результат от одного вычисления к другому и реализует стандартный интерфейс. И что бы ни случилось, результат всегда останется в той же монаде (даже, если произойдёт сбой, fail). | |||
Самая простая программистская аналогия монадам, которую я придумал, это конвееры (pipes) в командной оболочке Unix. Монады обеспечивают однонаправленный конвеер для вычислений. То же самое делают и конвееры в Unix. Например: | |||
<nowiki> | |||
$ seq 1 5 | awk '{print $1 " " $1*$1*$1 ;}' | |||
1 12 83 274 645 125</nowiki> | |||
seq создаёт список целых чисел. awk вычислят куб каждого из них. Что здесь замечательного? У нас есть две слабо связанные друг с другом программы, которым мы можем легко указать работать вместе. Поток текста создаваемый программой слева попадает по конвееру в программу справа, которая может читать этот поток, что-то с ним делать и создавать уже новый текстовый поток. Текстовый поток — общий формат для результата вычислений, | — операция, связывающая их воедино. | |||
Применение: | |||
* MayBe -привязка точной информации об ошибке к оборачиваемой функции; | |||
* Continuation - связывание нескольких функций между собой | |||
* Writer - привязка текстовой информации к функции, например логгинга | |||
* I/O - спросить пользователя, дождаться ответа из терминала, отреагировать на ответ; или прочитать файл, дождаться когда он будет доступен, прочитать содержимое, закрыть файл; | |||
* Identity - привязка/подмена информации в возвращаемом значении; | |||
* State - привязка состояния к функции | |||
* другие (смотрите ссылки [https://ru.wikipedia.org/wiki/%D0%9C%D0%BE%D0%BD%D0%B0%D0%B4%D0%B0_(%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5) в русской статье на википедии] и раздел «Ссылки» статьи на английском). | |||
==Парадигмы (стили) программирования== | |||
*[http://functionaljavascript.blogspot.ru/ Источник] | |||
Языки программирования состоят из операторов, условных операторов, операторов цикла и функций. Наличие условных операторов и операторов циклов являются отличительными чертами "императивных языков программирования". Функциональные языки, как правило, поддерживают только операторы и функции. | Языки программирования состоят из операторов, условных операторов, операторов цикла и функций. Наличие условных операторов и операторов циклов являются отличительными чертами "императивных языков программирования". Функциональные языки, как правило, поддерживают только операторы и функции. | ||
Интересно, что ни один из трех языков, Java, C ++ и C, не являются функциональными языками программирования.Язык C - императивный язык программирования, C ++ и Java | Интересно, что ни один из трех языков, Java, C ++ и C, не являются функциональными языками программирования.Язык C - императивный язык программирования, C ++ и Java императивно / объектно- ориентированные языки программирования. Т.е. существуют три парадигмы (стиля) программирования: императивный, объектно-ориентированный и функциональный. Существует еще один, декларативный стиль. | ||
Различия между этими парадигмами заложены в основе. | Различия между этими парадигмами заложены в основе. Императивное и объектно-ориентированное программирование основаны на «машине Тьюринга». Функциональное программирование на базе "лямбда-исчисления", а декларативное программирование основано на «логике первого порядка". Будут рассмотрены различия между, императивным, объектно-ориентированным и функциональном программировании на практическом уровне. | ||
В императивном языке программирования изменение состояния программы достигается путем выполнения серии операторов | В императивном языке программирования изменение состояния программы достигается путем выполнения серии операторов и контролирует поток, прежде всего, с помощью условных операторов, операторов цикла и вызовов функций. Программа, приведенная ниже, простая реализация метода JavaScript Array.join в императивном стиле. | ||
<nowiki>function simpleJoin(stringArray) { | <nowiki>function simpleJoin(stringArray) { | ||
Строка 19: | Строка 40: | ||
}</nowiki> | }</nowiki> | ||
Код выше - последовательный. Мы перебераем массив и | Код выше - последовательный. Мы перебераем массив и добавляем каждый элемент в строку-аккумулятор и возвращаем её. Сейчас мы перепишем эту функцию в объектно-ориентированном способом. Так как JavaScript имеет класс Array, мы добавим этот метод для класса Array, так что каждый экземпляр этого класса получит доступ к этой функции. JavaScript использует наследование через прототипы и поэтому мы добавляем эту функцию в прототип класса Array. | ||
<nowiki>Array.prototype.simpleJoin = function() { | |||
var accumulator = ""; | |||
for (var i=0, l=this.length; i < l; i++) { | |||
accumulator = accumulator + this[i]; | |||
} | |||
return accumulator; | |||
}</nowiki> | |||
Объектно-ориентированный вариант похож на императивную версию, кроме того, что функция теперь метод класса. Объектно-ориентированные языки, как правило, императивные также. | |||
Теперь запишем функциональную версию этой функции. | |||
<nowiki>function simpleJoin(stringArray, i, accumulator) { | |||
if (i === stringArray.length) { | |||
return accumulator; | |||
} else { | |||
return simpleJoin(stringArray, i+1, accumulator+stringArray[i]) | |||
} | |||
}</nowiki> | |||
Первое, что нужно отметить, это то, что для итерации не используется цикл . Вместо этого для итерации используется рекурсия. Действительно, это одна из характеристик функционального языка программирования. | |||
При первом вызове функции для массива stringArray: i установлено в 0 и accumulator установлен в "". Второй раз функции вызывается из себя с тем же stringArray, i установлен на i + 1 и accumulator равен accumulator + StringArray [i]. И мы продолжаем, пока i === stringArray.length, тогда мы вернем accumulator. Мы будем обсуждать рекурсию подробно позже. Просто помните, здесь использовалась рекурсия для итерации. | |||
Но осталось кое-что еще от императивного стиля - условный оператор. Функциональные языки, как правило, используют выражения вычисляющие какое-либо значение, вместо выражений, которые ничего не вычисляют. Итак, давайте перепишем функцию, чтобы сделать его как функциональные, как можно в JavaScript. | |||
<nowiki>function simpleJoin(stringArray, i, accumulator) { | |||
return (i === stringArray.length) ? accumulator : | |||
simpleJoin(stringArray, i + 1, accumulator + stringArray[i]) | |||
}</nowiki> | |||
Теперь это функциональный стиль, как его можно написать с помощью JavaScript. Вместо того, чтобы, возвращать значение на основе вычислений условного оператора, мы возвращаем значение вычисляемое условным оператором. Значение первого выражения возвращается, если верно и второе, если ложно. | |||
Мы видим, что функциональная версия короче. Действительно, одним из преимуществ функционального программирования является то, что необходимо меньшей кода для того что бы сделать то же самое, что приводит к лучшей читаемости и ремонтопригодности. | |||
'''Однако в случае JavaScript, как здесь, нельзя использовать рекурсию для итерации(прим. хвостовые рекурсии не поддерживает язык)'''. Вы должны продолжать использовать императивный или объектно-ориентированного метода для итерации. Это потому, что JavaScript (пока) не поддерживает "оптимизацию хвостового вызова". Мы будем обсуждать хвостовую рекурсию, и хвостовую оптимизацию, и как обойти эту проблему позже. JavaScript - [https://ru.wikipedia.org/wiki/%D0%9C%D1%83%D0%BB%D1%8C%D1%82%D0%B8%D0%BF%D0%B0%D1%80%D0%B0%D0%B4%D0%B8%D0%B3%D0%BC%D0%B0%D0%BB%D1%8C%D0%BD%D1%8B%D0%B9_%D1%8F%D0%B7%D1%8B%D0%BA_%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F мультипарадигмальный язык программирования]:императивный, объектно-ориентированный и функциональный язык. | |||
Хорошим примером мультипарадигмальной природы JavaScript является метод Array.forEach. Обратите внимание, что все современные браузеры уже реализовали это. Вот простая реализация. | |||
<nowiki>Array.prototype.forEach = function(callback) { | |||
for (var i = 0, len = this.length; i < len; ++i) { | |||
callback(this[i], i, this); | |||
} | |||
}</nowiki> | |||
В этом коде цикл for императивный код. Добавление в прототип объектно-ориентированное. Передача функции в качестве аргумента другой функции (callback) - функциональный код и эта возможность функционального программирования известна как “higher order function” (функции высшего порядка). В JavaScript, мы принимаем это как должное - передача функции в качестве аргумента. Удивительно этой возможности не было в самых популярных языках, до недавнего времени. например, вы не можете передавать функции в качестве аргументов в Java, хотя вы можете сделать это косвенно, через интерфейсы. То же самое в случае с C, хотя вы можете сделать это косвенно, используя указатели. | |||
==Функции первого класса и замыкания== | |||
Возможность функционального программирования, реализованые Бренданом Эйчом в JavaScript были '''first class functions''' или '''first class citizens'''. Это означает что функции рассматриваются как и все другие переменные. Т.е. можно передать их в качестве аргументов функций, вы можете вернуть их в качестве значений от других функций, или вы можете назначить им переменные или структуры данных. Мы видели ранее передачу функции в качестве аргумента. Вот пример присвоения функции переменной. | |||
<nowiki>function greet(name) { | |||
console.log("Hello " + name); | |||
} | |||
greet("John"); // "Hello John" | |||
var sayHello = greet; | |||
sayHello("Alex"); // "Hello Alex"</nowiki> | |||
Некоторые теоретики языка программирования рассматривают "анонимные функции" как функции первого класса. Чтобы не отстать, Брендан Эйч выбросил анонимные функции в миксе. Вот анонимная функция в JavaScript. | |||
<nowiki>function(name) { | |||
console.log(“Hello “ + name); | |||
}</nowiki> | |||
Если Вы заметили, мы не давали этой функции имя. Ведь это анонимная функция. Если вы попытаетесь запустить код, указанный выше, вы получите сообщение об ошибке. Что-то на подобие "вы не можете запустить код в этом контексте". И это правильно. Они могут быть назначены только на что-то, или передаться в качестве аргументов функции. | |||
<nowiki>var sayHello = function(name) { | |||
console.log(“Hello “ + name); | |||
} | |||
sayHello("Jane"); // "Hello Jane"</nowiki> | |||
Что делать, если мы хотим изменить приветствие? Иногда мы хотели бы сказать, "Hi", а не "Hello". Мы могли бы создать обобщенную функцию "createGreeting", которая, в свою очередь "сочинит" еще одну функцию для вас, и вернет новую комбинированную функцию. Так что, если мы бы хотели сказать "Hi" мы вернули функцию, а если мы хотели сказать "Hello" мы бы вернули другую функцию, которая говорит "Hello". Мы можем делать все это потому что JavaScript поддерживает функции первого класса, и мы можем вернуть функцию из другой функций. Вот код. | |||
<nowiki>function createGreeting(greeting) { | |||
return function(name) { | |||
console.log(greeting + " " + name); | |||
} | |||
} | |||
var sayHi = createGreeting("Hi"); | |||
sayHi("Jack"); // "Hi Jack" | |||
var sayHello = createGreeting("Hello"); | |||
sayHello("Jack"); // "Hello Jack"</nowiki> | |||
Функция createGreeting принимает greeting в качестве аргумента. Функция возвращает новую анонимную функцию. Однако созданная анонимная функция была создана внутри другой функции createGreeting. Таким образом она является '''вложенной функцией'''. Теперь, поскольку наш язык поддерживает анонимные функции, он также будет поддерживать вложенные функции. И когда мы возвращаемся вложенные функции из нашей функции мы столкнуться с другой проблемой. | |||
Анонимная функция принимает аргумент name и печатает в консоли greeting+name.Переменная name является аргументом анонимной функции, и ведет себя так же, как и любой другой переменной, определенной в функции. Другими словами name "локальна" для анонимной функции. Но это не относится к переменной greeting. Она определен в другой функции - createGreeting и, следовательно, "не локальна" для анонимной функции. Однако анонимные функции могут получить доступ к переменной greeting и это называется '''Лексическая область видимости'''. | |||
“Scope” (контекст, граница) переменной является её "видимостью" в пределах программы. "Лексическая область" означает, что видимость распространяется для всего текста (кода). Поэтому можно сказать “локальная переменная лексически ограничена” внутри функции, это значит, что локальные переменные функции видны для всего текста внутри функции, даже для кода внутри другой вложенной функции. Это также означает, что когда вы запускаете вложенную функцию вне окружения лексического контекста, the nested functions non local variable will not be visible. И в этом проблема возвращения вложенных функций из другой функции. И в самом деле вот что мы здесь делаем. | |||
<nowiki>var sayHi = createGreeting("Hi");</nowiki> | |||
В строке выше мы присваиваем возвращенную анонимную функцию переменной SayHi. И вызываем функцию в следующей строке. | |||
<nowiki>sayHi(“Jack”)</nowiki> | |||
Функция sayHi вызывается вне createGreeting. И переменная greeting недоступна вне createGreeting. Переменные доступные в контексте в котором они были определены, могут быть недоступны в контексте в котором они действительно вызываются. Вот почему языки, такие как C не поддерживает вложенные функции. Для того, чтобы так работать, язык должен поддерживать другой возможность функционального программирования под названием '''замыкание'''. JavaScript поддерживает замыкания. Любой язык, который поддерживает функции первого класса и вложенные функции должен поддерживать замыкания. | |||
Замыкание является ссылкой на все не локальные переменные функции. В предыдущем примере greeting была не локальной переменной, а name локальной. Замыкание-это таблица ссылок на все используемые в функции не локальные переменные. Это позволяет функции по-прежнему обращаться к не локальным переменным, даже если функция выходит из контекста этих переменных. | |||
==Функторы== | |||
Рассмотрим функцию. | |||
<nowiki>function plus1(value) { | |||
return value + 1 | |||
}</nowiki> | |||
Это просто функция, которая принимает целое число и добавляет единицу к нему. Аналогично создадим другую функция Plus2. Мы будем использовать эти функции позже. | |||
<nowiki>function plus2(value) { | |||
return value + 2 | |||
}</nowiki> | |||
И мы могли бы написать обобщенную функцию, чтобы использовать любую из этих функций, как и когда это требуется. | |||
<nowiki>function F(value, fn) { | |||
return fn(value) | |||
} | |||
F(1, plus1) ==>> 2</nowiki> | |||
Эта функция будет работать хорошо, пока значение передается целое. Попробуем массив. | |||
<nowiki>F([1, 2, 3], plus1) ==>> '1,2,31'</nowiki> | |||
Ой. Мы взяли массив целых чисел, добавили целое и получили строку! Не только он это сделал неправильную вещь, мы получили строку передав массив. Другими словами наша программа "испачкала" структуру ввода. Мы хотим чтобы F работала "правильно". Правильно, это "сохранить структуру" на выходе. | |||
Что значит "сохранить структуру"? Наша функция должна "развернуть" исходный массива и получить свои элементы. Затем вызвать переданную функцию для каждого элемента. Затем обернуть возвращаемые значения в новый массив и вернуть его. К счастью JavaScript имеет встроенную такую функцию - map. | |||
<nowiki>[1, 2, 3].map(plus1) ==>> [2, 3, 4]</nowiki> | |||
Функция map это функтор! | |||
'''Функтор''' - это функция, получающая данные и функцию на входе и сохраняющая структуру данных на выходе. | |||
Подробнее. | |||
Функтор является функцией, получающей данные и функцию, разворачивает данные, чтобы получить его элементы, вызывает функцию к каждому элементу, оборачивает возвращаемые значения в новую структуру, и возвращает новую структуру. | |||
Важно. В зависимости от типа данных, разворачивание может дать простые данные или структуру данных. Кроме того, возвращаемая структура не обязательно должна быть того же типа, что и исходная. В случае map и входные и возвращаемые данные имеют одинаковую структуру (массив). Возвращаемая структура может быть любого типа, так что бы можно было добраться до отдельных элементов структуры. Так что, если есть функция, которая принимает и массив и возвращает значение типа Object с массивом индексов в качестве ключей, и соответствующими значениями, также будут функтором. | |||
В случае JavaScript, функция filter - функтор, потому что он возвращает массив, однако forEach не функтор, потому что он возвращает undefined, т.е.. forEach не поддерживает структуры. | |||
Функторы пришли из теории категорий в математике, где функторы определяются как "гомоморфизм между категориями". Давайте расшифруем: | |||
*homo = то же самое | |||
*morphisms = функции, поддерживающие структуры | |||
*category = тип | |||
Согласно теории, функция F является функтор, когда для двух компонуемых обычных функции f и g выполняется равенство: | |||
<nowiki>F(f . g) = F(f) . F(g),</nowiki> | |||
где . (точка) обозначает композицию, т.е функторы должны сохранить композицию. | |||
Поэтому, учитывая это уравнение можно проверить, является данная функция действительно функтором или нет. | |||
===Array Functor=== | |||
Мы видели, что map это функтор, который действует по типу Array. Докажем, что JavaScript функция Array.map это функтор. | |||
<nowiki>function compose(f, g) { | |||
return function(x) {return f(g(x))} | |||
}</nowiki> | |||
Композиция функций выполняется из набора функций при помощи вызова следующей функции, с результатами предыдущей функции. Обратите внимание, что наша функция compose работает справа налево. g называется раньше, чем f. | |||
<nowiki> | |||
[1, 2, 3].map(compose(plus1, plus2)) ==>> [ 4, 5, 6 ] | |||
[1, 2, 3].map(plus2).map(plus1) ==>> [ 4, 5, 6 ]</nowiki> | |||
Да! Map действительно функтор. | |||
Попробуем некоторые функторы. Вы можете написать функторы для значений любого типа, до тех пор, как вы можете развернуть значение и вернуть структуру. | |||
===String Functor=== | |||
Можно ли написать функтор для типа строки? Можено развернуть строку? На самом деле можно, если думать о строке как о массиве символов. Все дело в том, как вы смотрите на значения. Также известно, что символы имеют текстовые коды, которые являются целыми числами. Таким образом, использовать plus1 для charcode символа, обернуть их обратно в строку, и вернуть её. | |||
<nowiki>function stringFunctor(value, fn) { | |||
var chars = value.split("") | |||
return chars.map(function(char) { | |||
return String.fromCharCode(fn(char.charCodeAt(0))) | |||
}).join("") | |||
} | |||
stringFunctor("ABCD", plus1) ==>> "BCDE"</nowiki> | |||
Вы можете начать видеть, как масштабны функторы. Вы можете фактически написать парсер, используя String Functor в качестве основы. | |||
===Function Functor=== | |||
В JavaScript функции являются first class citizens. Это означает, что вы можете обращаться к функции, как к любой другому значению. Можно ли написать функтор для значения типа функции? Да! Но как мы развернуть функцию? Функцию можно развернуть, вызвав её и получив возвращаемое ей значение. Но мы сразу столкнемся с проблемой. Для вызова функции мы должны передать аргументы. Помните, что этот функтор принимает только функцию в качестве аргумета. Мы можем решить эту проблему, вернув новую (анонимную) функцию. Новая (анонимная) функция будет вызвана с аргументами (initial) и мы, в свою очередь вернем анонимную функцию (без аргументов), в которой вызовем оригинальную функцию-аргумент (fn) в качестве параметров которой будет результат первой анонимной функции. (ЖЕСТЬ) | |||
<nowiki> | |||
function functionFunctor(value, fn) { | |||
return function(initial) { | |||
return function() { | |||
return fn(value(initial)) | |||
} | |||
} | |||
} | |||
var init = functionFunctor(function(x) {return x * x}, plus1) | |||
var final = init(2) | |||
final() ==> 5</nowiki> | |||
Наша Function Functor действительно ничего не делает. Но есть несколько моментов. Ничего не происходит, пока вы не вызовете final. Каждая функция находится в состоянии анабиоза, пока вы не вызовете финал. Function Functor формирует основу для более удивительным функциональной вещи как поддержание состояния, продолжение вызова и даже promises. Вы можете написать свои собственные Function Functor, чтобы сделать эти вещи! | |||
===MayBe Functor=== | |||
<nowiki>function mayBe(value, fn) { | |||
return value === null || value === undefined ? value : fn(value) | |||
}</nowiki> | |||
Да, это правильный функтор. | |||
<nowiki>mayBe(undefined, compose(plus1, plus2)) ==>> undefined | |||
mayBe(mayBe(undefined, plus2), plus1) ==>> undefined | |||
mayBe(1, compose(plus1, plus2)) ==>> 4 | |||
mayBe(mayBe(1, plus2), plus1) ==>> 4</nowiki> | |||
Итак mayBe проходит тест функтора. Здесь нет необходимости в разворачивании или оборачивании. Он просто возвращает ничего для ничего. mayBe полезно в качестве короткого замыкания, функции, которые можно использовать в качестве заменителя кода: | |||
<nowiki>if (result === null) { | |||
return null | |||
} else { | |||
doSomething(result) | |||
}</nowiki> | |||
===Identity Function=== | |||
<nowiki>function id(x) { | |||
return x | |||
}</nowiki> | |||
Функция выше, что известно как функции идентификации. Это просто функция, которая возвращает значение, переданное ему. Он называется так для тождественности в математическом аппарате. | |||
Мы узнали, что ранее функторы должны сохранить структуру. Однако то, что я не упомянул то, что функторы должны сохранить идентичность. т.е. | |||
<nowiki>F(value, id) = value</nowiki> | |||
Попробуем для map. | |||
<nowiki>[1, 2, 3].map(id) ==>> [ 1, 2, 3 ]</nowiki> | |||
===Типы сигнатур=== | |||
Тип сигнатуры функции это тип аргумента и возвращаемого значения. Тип сигнатуры для нашей plus1: | |||
<nowiki>f: int -> int</nowiki> | |||
Тип сигнатуры для map зависит от сигнатуры функции-аргумента. Так если map будет вызвана с функцией plus1, то её сигнатура: | |||
<nowiki>map: [int] -> [int]</nowiki> | |||
Но типы сигнатур | |||
Однако тип сигнатур функции-аргумента не обязательно должны быть такими же, как выше. Могут быть такие функции: | |||
<nowiki>f: int -> string</nowiki> | |||
из-за которой тип сигнатуры map бутет таким: | |||
<nowiki>map: [int] -> [string]</nowiki> | |||
Единственное ограничение в том, что изменение типа не влияет на композицию функтора. Таким образом, в общем тип сигнатуры функтора может быть таким: | |||
<nowiki>F: A -> B</nowiki> | |||
Другими словами map может принимать массив целых чисел и возвращть массив строк и это будет по-прежнему функтор. | |||
Монады особый случай функторов чей тип сигнатуры | |||
<nowiki>M: A -> A</nowiki> | |||
==Монады== | |||
Рассмотрим map functor из последней главы. Мы могли бы использовать map, чтобы перебрать два массивы добавление каждого элемента первого ко второму. | |||
<nowiki> | |||
var result = [1, 2].map(function(i) { | |||
return [3, 4].map(function(j) { | |||
return i + j | |||
}) | |||
}) | |||
console.log(result) ==>> [ [ 4, 5 ], [ 5, 6 ] ]</nowiki> | |||
Тип сигнатуры внутренней функции: | |||
<nowiki>f: int -> int</nowiki> | |||
и тип сигнатуры внутреннего map: | |||
<nowiki>map: [int] -> [int]</nowiki> | |||
Тип сигнатуры внешней функции: | |||
<nowiki> | |||
f: int -> [int]</nowiki> | |||
и тип сигнатуры внешнего map: | |||
<nowiki>map: [int] -> [[int]]</nowiki> | |||
Это правильное поведение функтора. Но это не то, что мы хотим. Мы хотим, чтобы результат был плоским, как показано ниже. | |||
<nowiki>[ 4, 5, 5, 6 ]</nowiki> | |||
===Array Monad=== | |||
Для того чтобы это произошло, тип сигнатуры функтора всегда должен быть ограничен | |||
<nowiki>F: [int] -> [int]</nowiki> | |||
Но функторы не предоставляют такое ограничение. Но монады да. Тип сигнатуры Array Monad: | |||
<nowiki>M: [T] -> [T]</nowiki> | |||
где T переданный тип. Вот почему map функтор, но не монада. Это еще не все. Мы должны поставить некоторые ограничения на тип данных, передаваемых в функцию. Функция не может возвращать любой тип. Мы можем решить эту проблему путем ограничения функции на возврат только типа массив. Так тип сигнатуры функции ограничен | |||
<nowiki>f: T -> [T]</nowiki> | |||
Эта функция известна как '''lift''', потому что она поднимает тип до требуемого. Это также известно как '''монадическая функция (mf)'''. И исходное значение переданное монаде называется '''монадическим значением (mv)'''. (Прим. т.е. аргументом mv для монады всегда массив, результат - тоже массив, причем строго того же типа что и аргумент mv.) Вот arrayMonad. | |||
<nowiki>function arrayMonad(mv, mf) { | |||
var result = [] | |||
mv.forEach(function(v) { | |||
result = result.concat(mf(v)) | |||
}) | |||
return result | |||
}</nowiki> | |||
Теперь можно использовать Array Monad для выполнения первого вычисления | |||
<nowiki>console.log(arrayMonad([1,2,3], function(i) { | |||
return [i + 1] | |||
})) ==>> [ 2, 3, 4 ]</nowiki> | |||
Обратите внимание, что наша монадическая функция заворачивает результат в массив [i + 1]. Теперь давайте попробуем для с двумерной задачи с которой мы начали. | |||
<nowiki>var result = arrayMonad([1, 2], function(i) { | |||
return arrayMonad([3, 4], function(j) { | |||
return [i + j] | |||
}) | |||
}) | |||
console.log(result) ==>> [ 4, 5, 5, 6 ]</nowiki> | |||
Теперь заметно превосходство монад над функторами. | |||
Мы можем написать общий двумерный итератор для массивов, которые примут два массива и функцию обратного вызова и применят её для каждого элемента обоих массивов. | |||
<nowiki>function forEach2d(array1, array2, callback) { | |||
return arrayMonad(array1, function(i) { | |||
return arrayMonad(array2, function(j) { | |||
return [callback(i,j)] | |||
}) | |||
}) | |||
}</nowiki> | |||
И мы можем попробовать эту функцию | |||
<nowiki>forEach2d([1, 2], [3,4], function(i, j) { | |||
return i + j | |||
}) ==>> [ 4, 5, 5, 6 ]</nowiki> | |||
Обратите внимание, что функция обратного вызова просто обычная функция, так что нам пришлось поднять(lift) её возвращаемые значения [callback(i,j)] в массив. Однако во всех монадах определена функция, чтобы сделать подъем. Её называют '''mResult'''. Добавим mResult к объекту функции arrayMonad. Функция concat не эффективна, поскольку она создает новый массив каждый раз. Взамен будем использовать array push. Вот окончательный код для Array Monad. | |||
<nowiki>function arrayMonad(mv, mf) { | |||
var result = [] | |||
mv.forEach(function(v) { | |||
Array.prototype.push.apply(result, mf(v)) | |||
}) | |||
return result | |||
} | |||
arrayMonad.mResult = function(v) { | |||
return [v] | |||
}</nowiki> | |||
и переписанная forEach2d | |||
<nowiki>function forEach2d(array1, array2, callback) { | |||
return arrayMonad(array1, function(i) { | |||
return arrayMonad(array2, function(j) { | |||
return arrayMonad.mResult(callback(i,j)) | |||
}) | |||
}) | |||
}</nowiki> | |||
Как упражнение попробуйте реализовать forEach3d. | |||
ArrayMonad представляет одноместную функцию и иначе известный как '''bind''' или '''mbind'''. Для того, что бы функция стала монадой, необходимо определить по крайней мере функции mbind и mresult. | |||
===Identity Monad=== | |||
Identity monad простейшая из всех монад, названная так потому, что это является идентичной mresult. | |||
<nowiki>function indentityMonad(mv, mf) { | |||
return mf(mv) | |||
} | |||
identityMonad.mResult = function(v) { | |||
return v | |||
}</nowiki> | |||
Она не очень полезная, но она правильная. | |||
===Maybe Monad=== | |||
Maybe Monad подобна identity monad, за исключением того, что он не будет вызывать монадическую функцию для значений null или undefined. На самом деле она сводится к функтору mayBe. | |||
<nowiki>function mayBeMonad(mv, mf) { | |||
return mv === null || mv === undefined || mv === false ? null : mf(mv) | |||
} | |||
mayBeMonad.mResult = function(v) { | |||
return v | |||
} | |||
</nowiki> | |||
===Законы монад=== | |||
====Первый закон монад==== | |||
<nowiki>M(mResult(x), mf) = mf(x)</nowiki> | |||
Который означает, что mResult должен сделать с x, чтобы превратить х в монадическое значение. М будет разворачивать это монадическое значение перед его применением в монадической функции mf. Давайте проверить это на нашем array monad. | |||
<nowiki>var x = 4; | |||
function mf(x) { | |||
return [x * 2] | |||
} | |||
arrayMonad(arrayMonad.mResult(x), mf) ==>> [ 8 ] | |||
mf(x) ==>> [ 8 ]</nowiki> | |||
====Второй закон монад==== | |||
<nowiki>M(mv, mResult) = mv</nowiki> | |||
Это означает, что mBind извлекает значение mv ,а mResult должен будет обернуть назад это значение обратно монадическое. Это гарантирует, что mResult является одноместной функцией. Давайте проверить его. Это эквивалентно сохранению идентичности в случае функтора. | |||
<nowiki>arrayMonad([1, 2, 3], arrayMonad.mResult) ==>> [ 1, 2, 3 ]</nowiki> | |||
====Третий закон монад==== | |||
<nowiki>M(M(mv, mf), mg)) = M(mv, function(x){return M(mf(x), mg)})</nowiki> | |||
Не имеет значения, если вы применяете mf к mv, а затем к mg, или применять mv к монадической функции, представляющей композицию mf и mg. | |||
<nowiki> | |||
function mg(x) { | |||
return [x * 3] | |||
} | |||
arrayMonad(arrayMonad([1, 2, 3], mf), mg) ==>> [ 6, 12, 18 ] | |||
arrayMonad([1, 2, 3], function(x) { | |||
return arrayMonad(mf(x), mg) | |||
}) ==>> [ 6, 12, 18 ] | |||
</nowiki> | |||
===doMonad=== | |||
Мы знаем, что монадическая функция принимает значение и возвращает монадическое значение. Монада принимает монадическое значение и монадическую функцию и возвращает монадическое значение. Что делать, если монадическая функция вызывает монаду с монадическим значением и самой себя, и возвращает результат? Это было бы правильная монадическая функция, потому что она возвращает монадическое значение. | |||
Функция doMonad делает именно это. Это передает в монаду массив монадических значений и callback в качестве аргументов. Это определяет монадическую функцию как рекурсивно вызывающую монаду для каждого монадического значения и саму себя. Цикл заканчивается, когда не остается монадических значений. Она возвращает callback с каждым развернутым значением монадических значений.Сallback cb каррируется в замыкании, называемой оберткой и становится видимой для mf. [https://ru.wikipedia.org/wiki/%D0%9A%D0%B0%D1%80%D1%80%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5 Каррирование] или карринг (англ. currying) в информатике — преобразование функции от многих аргументов в функцию, берущую свои аргументы по одному. | |||
<nowiki> | |||
function curry(fn, numArgs) { | |||
numArgs = numArgs || fn.length | |||
return function f(saved_args) { | |||
return function() { | |||
var args = saved_args.concat(Array.prototype.slice.call(arguments)) | |||
return args.length === numArgs ? fn.apply(null, args) : f(args) | |||
} | |||
}([]) | |||
} | |||
function doMonad(monad, values, cb) { | |||
function wrap(curriedCb, index) { | |||
return function mf(v) { | |||
return (index === values.length - 1) ? | |||
monad.mResult(curriedCb(v)) : | |||
monad(values[index + 1], wrap(curriedCb(v), index + 1)) | |||
} | |||
} | |||
return monad(values[0], wrap(curry(cb), 0)) | |||
} | |||
doMonad(arrayMonad, [[1, 2], [3, 4]], function(x, y) { | |||
return x + y | |||
}) //==>> [ 4, 5, 5, 6 ]</nowiki> | |||
Теперь нет необходимости в функции forEach2d , которую мы написали ранее! И лучшее еще впереди! | |||
===Пример использования doMonad для массивов=== | |||
Мы можем написать общую функцию FOR для массива, которая принимает набор массивов и обратный вызов для аргументов. | |||
<nowiki>function FOR() { | |||
var args = [].slice.call(arguments) | |||
callback = args.pop() | |||
return doMonad(arrayMonad, args, callback) | |||
} | |||
FOR([1, 2], [3, 4], function(x, y) { | |||
return x + y | |||
}) //==>> [ 4, 5, 5, 6 ] | |||
FOR([1, 2], [3, 4], [5, 6], function(x, y, z) { | |||
return x + y + z | |||
}) //==>> [ 9, 10, 10, 11, 10, 11, 11, 12 ]</nowiki> | |||
Удивительно! | |||
===State Monad=== | |||
Ранее рассматривался function fucntor, который принимал значения типа function. Аналогично монадические значения могут также быть функциями. Однако важно различать монадические функции и монадические значения, являющиеся функциями. Тип сигнатуры монадической функции | |||
<nowiki>mf: v -> mv</nowiki> | |||
т.е. принимает значение и поднимает его до монадического значения. Вспомним, что монадическое значение само по себе функция. Поэтому mf вернет функцию mv. | |||
Типы сигнатуры монадического значения, являющегося функцией, зависит от того, что делает сама эта функция. В случаеs state monad тип сигнатуры монадического значения: | |||
<nowiki>mv: state -> [value, new state]</nowiki> | |||
Функция в монадическом значении принимает состояние state и возвращает массив, содержащий данные value и новое состояние new state. Состояние state может быть любого типа: array, string, integer и т.д. | |||
StateMonad принимает монадическое значение и монадическое функцию и возвращает функцию, для которой мы должны задать исходное состояние. Исходное состояние передается mv, которое возвращает значение. Затем вызывается mf с этим значением и mf возвращает монадическое значение, которое является функцией. Мы должны вызвать эту функцию с NewState. Уф! | |||
<nowiki>function stateMonad(mv, mf) { | |||
return function(state) { | |||
var compute = mv(state) | |||
var value = compute[0] | |||
var newState = compute[1] | |||
return mf(value)(newState) | |||
} | |||
}</nowiki> | |||
mResult для state monad: | |||
<nowiki>stateMonad.mResult = function(value) { | |||
return function(state) { | |||
return [value, state]; | |||
} | |||
}</nowiki> | |||
(Примечание о применении: проверка на ошибку) | |||
===Parser Monad=== | |||
Парсер- это функция, которая принимает строку, анализирует её по некоторым критериям и возвращает совпавшую часть и остаток. Позволяет написать тип сигнатуры функции. | |||
<nowiki>parser: string -> [match, newstring]</nowiki> | |||
Это выглядит как монадическое значение state monad, с ограничением типа state строкой. Но это еще не все, анализатор вернется NULL, если строка не соответствует этим критериям. Так что давайте напишем Parser Monad, чтобы удивить разницу. | |||
<nowiki> | |||
function parserMonad(mv, mf) { | |||
return function(str) { | |||
var compute = mv(str) | |||
if (compute === null) { | |||
return null | |||
} else { | |||
return mf(compute[0])(compute[1]) | |||
} | |||
} | |||
} | |||
parserMonad.mResult = function(value) { | |||
return function(str) { | |||
return [value, str]; | |||
} | |||
}</nowiki> | |||
Как мы видели ранее, монады требует, чтобы вы определить по крайней мере две функции, mBind (саму функцию монаду) и mResult. Но это еще не все. При желании вы можете определить еще две функции, mZero и Mplus. | |||
mZero является определение «Nothing» для монады. например. для arrayMonad, mZero будет <nowiki>[ ]</nowiki>. В случае анализатора монады mZero определяется следующим образом. (mZero должен иметь тот же тип сигнатуры монадического значения). | |||
<nowiki>parserMonad.mZero = function(str) { | |||
return null | |||
}</nowiki> | |||
Mplus это функция, которая принимает монадические значения в качестве аргументов, и игнорирует mZero среди них. Как принятые значения обрабатываются зависит от каждой монады. Для parser monad, mZero будет взять набор анализаторов (монадические значения Parser монады) и вернет значение, возвращенное первым парсером который вернет значение не mZero (NULL). | |||
<nowiki> | |||
parserMonad.mPlus = function() { | |||
var parsers = Array.prototype.slice.call(arguments) | |||
return function(str) { | |||
var result, i | |||
for (i = 0; i < parsers.length; ++i) { | |||
result = parsers[i](str) | |||
if (result !== null) { | |||
break; | |||
} | |||
} | |||
return result | |||
} | |||
}</nowiki> | |||
===Continuation Monad=== | |||
Continuation monad проста для понимания. В задании о композиции функций была рассмотрена композиция двух функций f и g: | |||
<nowiki>(f . g) = f(g(x))</nowiki> | |||
Функция f называется продолжением функции g. | |||
Также известно, что можно обернуть значение в функцию, создав замыкание. В примере ниже, вложенная имеет доступ к value, обернутому в замыкание. | |||
<nowiki>function(value) { | |||
return function() { | |||
// value can be accessed here | |||
} | |||
}</nowiki> | |||
Монадическое значение continuation monad является функцией, которая возвращает функцию-продолжение, вызывая её со значением из собственной "обёртки". | |||
<nowiki> | |||
function(continuation) { | |||
return continuation(value) | |||
}</nowiki> | |||
Функция mResult этой монады принимает простое значение и "поднимает" его до монадического значения. Так будет выглядеть функция mResult для continuation monad. | |||
<nowiki>var mResult = function(value) { | |||
return function(continuation) { | |||
return continuation(value) | |||
} | |||
}</nowiki> | |||
Итак mResult это функция, которая принимает простое значение и возвращает монадическое значение, которое будет передано в функцию-продолжение. | |||
Сама continuation monad или mBind более сложная. | |||
<nowiki>var continuationMonad = function(mv, mf) { | |||
return function(continuation) { | |||
// we will add to here next | |||
} | |||
}</nowiki> | |||
Первым вызовом возвращается функция, которую нужно вызвать с функцией-продолжением. Это просто. Но как развернуть value внутри mv? mv принимает функцию-продолжение , но вызов mv с функцией-продолжением не происходит. Мы должны развернуть значение в mv и вызвать mf первым. Таким образом, мы должны обмануть mv в передаче нам значение путем вызова его с нашей собственной функцией-продолжения. | |||
<nowiki>mv(function(value) { | |||
// gotcha! the value | |||
})</nowiki> | |||
Добавим эту функцию в код | |||
<nowiki> | |||
var continuationMonad = function(mv, mf) { | |||
return function(continuation) { | |||
return mv(function(value) { | |||
// gotcha! the value | |||
}) | |||
} | |||
}</nowiki> | |||
Теперь все что мы должны сделать - это вызвать mf со значением. Мы знаем, что монадическая функция принимает значение и возвращает монадическое значение. Так мы называем возвращаемое монадическое значение из функции mf с продолжением. Уф! Вот полный код для продолжения монады. | |||
<nowiki> | |||
var continuationMonad = function(mv, mf) { | |||
return function(continuation) { | |||
return mv(function(value) { | |||
return mf(value)(continuation) | |||
}) | |||
} | |||
} | |||
continuationMonad.mResult = function(value) { | |||
return function(continuation) { | |||
return continuation(value) | |||
} | |||
}</nowiki> | |||
==Библиотека== | |||
[https://github.com/santoshrajan/monadjs Источник] | |||
===Код=== | |||
<nowiki>/* | |||
monadjs | |||
Monad Library for JavaScript | |||
Copyright (c) 2013 Santosh Rajan | |||
License - MIT - https://github.com/santoshrajan/monadjs/blob/master/LICENSE | |||
*/ | |||
exports.version = "0.1.0" | |||
// Curry function | |||
function curry(fn, numArgs) { | |||
numArgs = numArgs || fn.length | |||
return function f(saved_args) { | |||
return function() { | |||
var args = saved_args.concat(Array.prototype.slice.call(arguments)) | |||
return args.length === numArgs ? fn.apply(null, args) : f(args) | |||
} | |||
}([]) | |||
} | |||
// The identity Monad | |||
function identityMonad(mv, mf) { | |||
return mf(mv) | |||
} | |||
identityMonad.mResult = function(v) { | |||
return v | |||
} | |||
// The mayBe Monad | |||
function mayBeMonad(mv, mf) { | |||
return mv === null || mv === undefined || mv === false ? null : mf(mv) | |||
} | |||
mayBeMonad.mResult = function(v) { | |||
return v | |||
} | |||
// The array Monad | |||
function arrayMonad(mv, mf) { | |||
var result = [] | |||
mv.forEach(function(v) { | |||
Array.prototype.push.apply(result, mf(v)) | |||
}) | |||
return result | |||
} | |||
arrayMonad.mResult = function(v) { | |||
return [v] | |||
} | |||
// The state Monad | |||
function stateMonad(mv, mf) { | |||
return function(state) { | |||
var compute = mv(state) | |||
return mf(compute[0])(compute[1]) | |||
} | |||
} | |||
stateMonad.mResult = function(value) { | |||
return function(state) { | |||
return [value, state]; | |||
} | |||
} | |||
// The parser Monad | |||
function parserMonad(mv, mf) { | |||
return function(str) { | |||
var compute = mv(str) | |||
if (compute === null) { | |||
return null | |||
} else { | |||
return mf(compute[0])(compute[1]) | |||
} | |||
} | |||
} | |||
parserMonad.mResult = function(value) { | |||
return function(str) { | |||
return [value, str]; | |||
} | |||
} | |||
parserMonad.mZero = function(str) { | |||
return null | |||
} | |||
parserMonad.mPlus = function() { | |||
var parsers = Array.prototype.slice.call(arguments) | |||
return function(str) { | |||
var result, i | |||
for (i = 0; i < parsers.length; ++i) { | |||
result = parsers[i](str) | |||
if (result !== null) { | |||
break; | |||
} | |||
} | |||
return result | |||
} | |||
} | |||
// The continuation Monad | |||
function continuationMonad(mv, mf) { | |||
return function(continuation) { | |||
return mv(function(value) { | |||
return mf(value)(continuation); | |||
}) | |||
} | |||
} | |||
continuationMonad.mResult = function(value) { | |||
return function(continuation) { | |||
return continuation(value) | |||
} | |||
} | |||
function doMonad(monad, values, cb) { | |||
function wrap(curriedCb, index) { | |||
return function mf(v) { | |||
return (index === values.length - 1) ? | |||
monad.mResult(curriedCb(v)) : | |||
monad(values[index + 1], wrap(curriedCb(v), index + 1)) | |||
} | |||
} | |||
return monad(values[0], wrap(curry(cb), 0)) | |||
} | |||
exports.identity = identityMonad | |||
exports.mayBe = mayBeMonad | |||
exports.array = arrayMonad | |||
exports.state = stateMonad | |||
exports.parser = parserMonad | |||
exports.continuation = continuationMonad | |||
exports.do = doMonad</nowiki> | |||
===Примеры=== | |||
====arrayMonad.js==== | |||
<nowiki>var monads = require("monadjs"); | |||
function forEach3D(iArray, jArray, kArray, callback) { | |||
return monads.do(monads.array, [iArray, jArray, kArray], callback) | |||
} | |||
var result = forEach3D([1, 2], [3, 4], [5, 6], function(i, j, k) { | |||
return i + j + k | |||
}) | |||
console.log(result)</nowiki> | |||
====identitymonad.js==== | |||
<nowiki>var monads = require("monadjs") | |||
var result = monads.do(monads.identity, [1, 2], function(a, b) { | |||
return a + b | |||
}) | |||
console.log(result);</nowiki> | |||
====maybemonad.js==== | |||
<nowiki>var monads = require("monadjs"); | |||
var push = function(element) { | |||
return function(state) { | |||
var newstate = [element] | |||
return [undefined, newstate.concat(state)] | |||
} | |||
} | |||
var pop = function() { | |||
return function(state) { | |||
var newstate = state.slice(1) | |||
return [state[0], newstate] | |||
} | |||
} | |||
var result = monads.do(monads.state, | |||
[ | |||
push(5), | |||
push(10), | |||
push(20), | |||
pop() | |||
], | |||
function(val1, val2, val3, val4) { | |||
return val4 | |||
} | |||
) | |||
console.log(result([]))</nowiki> | |||
====statemonad.js==== | |||
<nowiki>var monads = require("monadjs"); | |||
var push = function(element) { | |||
return function(state) { | |||
var newstate = [element] | |||
return [undefined, newstate.concat(state)] | |||
} | |||
} | |||
var pop = function() { | |||
return function(state) { | |||
var newstate = state.slice(1) | |||
return [state[0], newstate] | |||
} | |||
} | |||
var result = monads.do(monads.state, | |||
[ | |||
push(5), | |||
push(10), | |||
push(20), | |||
pop() | |||
], | |||
function(val1, val2, val3, val4) { | |||
return val4 | |||
} | |||
) | |||
console.log(result([]))</nowiki> | |||
==Конструктор монад== | |||
[https://github.com/douglascrockford/monad/blob/master/monad.js Источник] | |||
<nowiki>// monad.js | |||
// Douglas Crockford | |||
// 2015-05-02 | |||
// Public Domain | |||
// The MONAD function is a macroid that produces monad constructor functions. | |||
// It can take an optional modifier function, which is a function that is | |||
// allowed to modify new monads at the end of the construction processes. | |||
// A monad constructor (sometimes called 'unit' or 'return' in some mythologies) | |||
// comes with three methods, lift, lift_value, and method, all of which can add | |||
// methods and properties to the monad's prototype. | |||
// A monad has a 'bind' method that takes a function that receives a value and | |||
// is usually expected to return a monad. | |||
// var identity = MONAD(); | |||
// var monad = identity("Hello world."); | |||
// monad.bind(alert); | |||
// var ajax = MONAD() | |||
// .lift('alert', alert); | |||
// var monad = ajax("Hello world."); | |||
// monad.alert(); | |||
// var maybe = MONAD(function (monad, value) { | |||
// if (value === null || value === undefined) { | |||
// monad.is_null = true; | |||
// monad.bind = function () { | |||
// return monad; | |||
// }; | |||
// return null; | |||
// } | |||
// return value; | |||
// }); | |||
// var monad = maybe(null); | |||
// monad.bind(alert); // Nothing happens. | |||
/*jslint this */ | |||
function MONAD(modifier) { | |||
'use strict'; | |||
// Each unit constructor has a monad prototype. The prototype will contain an | |||
// is_monad property for classification, as well as all inheritable methods. | |||
var prototype = Object.create(null); | |||
prototype.is_monad = true; | |||
// Each call to MONAD will produce a new unit constructor function. | |||
function unit(value) { | |||
// Construct a new monad. | |||
var monad = Object.create(prototype); | |||
// In some mythologies 'bind' is called 'pipe' or '>>='. | |||
// The bind method will deliver the unit's value parameter to a function. | |||
monad.bind = function (func, args) { | |||
// bind takes a function and an optional array of arguments. It calls that | |||
// function passing the monad's value and bind's optional array of args. | |||
// With ES6, this horrible return statement can be replaced with | |||
// return func(value, ...args); | |||
return func.apply( | |||
undefined, | |||
[value].concat(Array.prototype.slice.apply(args || [])) | |||
); | |||
}; | |||
// If MONAD's modifier parameter is a function, then call it, passing the monad | |||
// and the value. | |||
if (typeof modifier === 'function') { | |||
value = modifier(monad, value); | |||
} | |||
// Return the shiny new monad. | |||
return monad; | |||
} | |||
unit.method = function (name, func) { | |||
// Add a method to the prototype. | |||
prototype[name] = func; | |||
return unit; | |||
}; | |||
unit.lift_value = function (name, func) { | |||
// Add a method to the prototype that calls bind with the func. This can be | |||
// used for ajax methods that return values other than monads. | |||
prototype[name] = function () { | |||
return this.bind(func, arguments); | |||
}; | |||
return unit; | |||
}; | |||
unit.lift = function (name, func) { | |||
// Add a method to the prototype that calls bind with the func. If the value | |||
// returned by the func is not a monad, then make a monad. | |||
prototype[name] = function () { | |||
var result = this.bind(func, arguments); | |||
return result && result.is_monad === true | |||
? result | |||
: unit(result); | |||
}; | |||
return unit; | |||
}; | |||
return unit; | |||
}</nowiki> | |||
==Конструктор Promise== | |||
[https://github.com/douglascrockford/monad/blob/master/vow.js источник] | |||
<nowiki>// vow.js | |||
// Douglas Crockford | |||
// 2015-05-02 | |||
// Public Domain | |||
/*global setImmediate */ | |||
var VOW = (function () { | |||
'use strict'; | |||
// The VOW object contains a .make function that is used to make vows. | |||
// It may also contain other useful functions. | |||
// In some mythologies, 'VOW' is called 'deferrer'. | |||
function enlighten(queue, fate) { | |||
// enlighten is a helper function of herald and .when. It schedules the | |||
// processing of all of the resolution functions in either the keepers queue | |||
// or the breakers queue in later turns with the promise's fate. | |||
queue.forEach(function (func) { | |||
setImmediate(func, fate); | |||
}); | |||
} | |||
return { | |||
make: function make() { | |||
// The make function makes new vows. A vow contains a promise object and the | |||
// two resolution functions (break and keep) that determine the fate of the | |||
// promise. | |||
var breakers = [], // .when's broken queue | |||
fate, // The promise's ultimate value | |||
keepers = [], // .when's kept queue | |||
status = 'pending'; // 'broken', 'kept', or 'pending' | |||
function enqueue( | |||
resolution, // 'keep' or 'break' | |||
func, // A function that was registered with .when | |||
vow // A vow that provides the resolution functions | |||
) { | |||
// enqueue is a helper function used by .when. It will append a function to | |||
// either the keepers queue or the breakers queue. | |||
var queue = resolution === 'keep' | |||
? keepers | |||
: breakers; | |||
queue[queue.length] = typeof func !== 'function' | |||
// If func is not a function, push the resolver so that the value passes to | |||
// the next cascaded .when. | |||
? vow[resolution] | |||
// If the func is a function, push a function that calls func with a value. | |||
// The result can be a promise, or not a promise, or an exception. | |||
: function (value) { | |||
try { | |||
var result = func(value); | |||
// If the result is a promise, then register our resolver with that promise. | |||
if (result && result.is_promise === true) { | |||
result.when(vow.keep, vow.break); | |||
// But if it is not a promise, then use the result to resolve our promise. | |||
} else { | |||
vow.keep(result); | |||
} | |||
// But if func throws an exception, then break our promise. | |||
} catch (e) { | |||
vow.break(e); | |||
} | |||
}; | |||
} | |||
function herald(state, value, queue) { | |||
// The herald function is a helper function of break and keep. | |||
// It seals the promise's fate, updates its status, enlightens | |||
// one of the queues, and empties both queues. | |||
if (status !== 'pending') { | |||
throw "overpromise"; | |||
} | |||
fate = value; | |||
status = state; | |||
enlighten(queue, fate); | |||
keepers.length = 0; | |||
breakers.length = 0; | |||
} | |||
// Construct and return the vow object. | |||
return { | |||
'break': function (value) { | |||
// The break method breaks the promise. | |||
herald('broken', value, breakers); | |||
}, | |||
keep: function keep(value) { | |||
// The keep method keeps the promise. | |||
herald('kept', value, keepers); | |||
}, | |||
promise: { | |||
// The promise is an object with a .when method. | |||
is_promise: true, | |||
// The .when method is the promise monad's bind. The .when method can take two | |||
// optional functions. One of those functions may be called, depending on the | |||
// promise's resolution. Both could be called if the the kept function throws. | |||
when: function (kept, broken) { | |||
// Make a new vow. Return the new promise. | |||
var vow = make(); | |||
switch (status) { | |||
// If this promise is still pending, then enqueue both kept and broken. | |||
case 'pending': | |||
enqueue('keep', kept, vow); | |||
enqueue('break', broken, vow); | |||
break; | |||
// If the promise has already been kept, then enqueue only the kept function, | |||
// and enlighten it. | |||
case 'kept': | |||
enqueue('keep', kept, vow); | |||
enlighten(keepers, fate); | |||
break; | |||
// If the promise has already been broken, then enqueue only the broken | |||
// function, and enlighten it. | |||
case 'broken': | |||
enqueue('break', broken, vow); | |||
enlighten(breakers, fate); | |||
break; | |||
} | |||
return vow.promise; | |||
} | |||
} | |||
}; | |||
}, | |||
every: function every(array) { | |||
// The every function takes an array of promises and returns a promise that | |||
// will deliver an array of results only if every promise is kept. | |||
var remaining = array.length, results = [], vow = VOW.make(); | |||
if (!remaining) { | |||
vow.break(array); | |||
} else { | |||
array.forEach(function (promise, i) { | |||
promise.when(function (value) { | |||
results[i] = value; | |||
remaining -= 1; | |||
if (remaining === 0) { | |||
vow.keep(results); | |||
} | |||
}, function (reason) { | |||
remaining = NaN; | |||
vow.break(reason); | |||
}); | |||
}); | |||
} | |||
return vow.promise; | |||
}, | |||
first: function first(array) { | |||
// The first function takes an array of promises and returns a promise to | |||
// deliver the first observed kept promise, or a broken promise if all of | |||
// the promises are broken. | |||
var found = false, remaining = array.length, vow = VOW.make(); | |||
function check() { | |||
remaining -= 1; | |||
if (remaining === 0 && !found) { | |||
vow.break(); | |||
} | |||
} | |||
if (remaining === 0) { | |||
vow.break(array); | |||
} else { | |||
array.forEach(function (promise) { | |||
promise.when(function (value) { | |||
if (!found) { | |||
found = true; | |||
vow.keep(value); | |||
} | |||
check(); | |||
}, check); | |||
}); | |||
} | |||
return vow.promise; | |||
}, | |||
any: function any(array) { | |||
// The any function takes an array of promises and returns a promise that | |||
// will deliver a possibly sparse array of results of any kept promises. | |||
// The result will contain an undefined element for each broken promise. | |||
var remaining = array.length, results = [], vow = VOW.make(); | |||
function check() { | |||
remaining -= 1; | |||
if (remaining === 0) { | |||
vow.keep(results); | |||
} | |||
} | |||
// vow.js | |||
// Douglas Crockford | |||
// 2015-05-02 | |||
// Public Domain | |||
/*global setImmediate */ | |||
var VOW = (function () { | |||
'use strict'; | |||
// The VOW object contains a .make function that is used to make vows. | |||
// It may also contain other useful functions. | |||
// In some mythologies, 'VOW' is called 'deferrer'. | |||
function enlighten(queue, fate) { | |||
// enlighten is a helper function of herald and .when. It schedules the | |||
// processing of all of the resolution functions in either the keepers queue | |||
// or the breakers queue in later turns with the promise's fate. | |||
queue.forEach(function (func) { | |||
setImmediate(func, fate); | |||
}); | |||
} | |||
return { | |||
make: function make() { | |||
// The make function makes new vows. A vow contains a promise object and the | |||
// two resolution functions (break and keep) that determine the fate of the | |||
// promise. | |||
var breakers = [], // .when's broken queue | |||
fate, // The promise's ultimate value | |||
keepers = [], // .when's kept queue | |||
status = 'pending'; // 'broken', 'kept', or 'pending' | |||
function enqueue( | |||
resolution, // 'keep' or 'break' | |||
func, // A function that was registered with .when | |||
vow // A vow that provides the resolution functions | |||
) { | |||
// enqueue is a helper function used by .when. It will append a function to | |||
// either the keepers queue or the breakers queue. | |||
var queue = resolution === 'keep' | |||
? keepers | |||
: breakers; | |||
queue[queue.length] = typeof func !== 'function' | |||
// If func is not a function, push the resolver so that the value passes to | |||
// the next cascaded .when. | |||
? vow[resolution] | |||
// If the func is a function, push a function that calls func with a value. | |||
// The result can be a promise, or not a promise, or an exception. | |||
: function (value) { | |||
try { | |||
var result = func(value); | |||
// If the result is a promise, then register our resolver with that promise. | |||
if (result && result.is_promise === true) { | |||
result.when(vow.keep, vow.break); | |||
// But if it is not a promise, then use the result to resolve our promise. | |||
} else { | |||
vow.keep(result); | |||
} | |||
// But if func throws an exception, then break our promise. | |||
} catch (e) { | |||
vow.break(e); | |||
} | |||
}; | |||
} | |||
function herald(state, value, queue) { | |||
// The herald function is a helper function of break and keep. | |||
// It seals the promise's fate, updates its status, enlightens | |||
// one of the queues, and empties both queues. | |||
if (status !== 'pending') { | |||
throw "overpromise"; | |||
} | |||
fate = value; | |||
status = state; | |||
enlighten(queue, fate); | |||
keepers.length = 0; | |||
breakers.length = 0; | |||
} | |||
// Construct and return the vow object. | |||
return { | |||
'break': function (value) { | |||
// The break method breaks the promise. | |||
herald('broken', value, breakers); | |||
}, | |||
keep: function keep(value) { | |||
// The keep method keeps the promise. | |||
herald('kept', value, keepers); | |||
}, | |||
promise: { | |||
// The promise is an object with a .when method. | |||
is_promise: true, | |||
// The .when method is the promise monad's bind. The .when method can take two | |||
// optional functions. One of those functions may be called, depending on the | |||
// promise's resolution. Both could be called if the the kept function throws. | |||
when: function (kept, broken) { | |||
// Make a new vow. Return the new promise. | |||
var vow = make(); | |||
switch (status) { | |||
// If this promise is still pending, then enqueue both kept and broken. | |||
case 'pending': | |||
enqueue('keep', kept, vow); | |||
enqueue('break', broken, vow); | |||
break; | |||
// If the promise has already been kept, then enqueue only the kept function, | |||
// and enlighten it. | |||
case 'kept': | |||
enqueue('keep', kept, vow); | |||
enlighten(keepers, fate); | |||
break; | |||
// If the promise has already been broken, then enqueue only the broken | |||
// function, and enlighten it. | |||
case 'broken': | |||
enqueue('break', broken, vow); | |||
enlighten(breakers, fate); | |||
break; | |||
} | |||
return vow.promise; | |||
} | |||
} | |||
}; | |||
}, | |||
every: function every(array) { | |||
// The every function takes an array of promises and returns a promise that | |||
// will deliver an array of results only if every promise is kept. | |||
var remaining = array.length, results = [], vow = VOW.make(); | |||
if (!remaining) { | |||
vow.break(array); | |||
} else { | |||
array.forEach(function (promise, i) { | |||
promise.when(function (value) { | |||
results[i] = value; | |||
remaining -= 1; | |||
if (remaining === 0) { | |||
vow.keep(results); | |||
} | |||
}, function (reason) { | |||
remaining = NaN; | |||
vow.break(reason); | |||
}); | |||
}); | |||
} | |||
return vow.promise; | |||
}, | |||
first: function first(array) { | |||
// The first function takes an array of promises and returns a promise to | |||
// deliver the first observed kept promise, or a broken promise if all of | |||
// the promises are broken. | |||
var found = false, remaining = array.length, vow = VOW.make(); | |||
function check() { | |||
remaining -= 1; | |||
if (remaining === 0 && !found) { | |||
vow.break(); | |||
} | |||
} | |||
if (remaining === 0) { | |||
vow.break(array); | |||
} else { | |||
array.forEach(function (promise) { | |||
promise.when(function (value) { | |||
if (!found) { | |||
found = true; | |||
vow.keep(value); | |||
} | |||
check(); | |||
}, check); | |||
}); | |||
} | |||
return vow.promise; | |||
}, | |||
any: function any(array) { | |||
// The any function takes an array of promises and returns a promise that | |||
// will deliver a possibly sparse array of results of any kept promises. | |||
// The result will contain an undefined element for each broken promise. | |||
var remaining = array.length, results = [], vow = VOW.make(); | |||
function check() { | |||
remaining -= 1; | |||
if (remaining === 0) { | |||
vow.keep(results); | |||
} | |||
} | |||
if (!remaining) { | |||
vow.keep(results); | |||
} else { | |||
array.forEach(function (promise, i) { | |||
promise.when(function (value) { | |||
results[i] = value; | |||
check(); | |||
}, check); | |||
}); | |||
} | |||
return vow.promise; | |||
}, | |||
kept: function (value) { | |||
// Returns a new kept promise. | |||
var vow = VOW.make(); | |||
vow.keep(value); | |||
return vow.promise; | |||
}, | |||
broken: function (reason) { | |||
// Returns a new broken promise. | |||
var vow = VOW.make(); | |||
vow.break(reason); | |||
return vow.promise; | |||
} | |||
}; | |||
}()); | |||
if (!remaining) { | |||
vow.keep(results); | |||
} else { | |||
array.forEach(function (promise, i) { | |||
promise.when(function (value) { | |||
results[i] = value; | |||
check(); | |||
}, check); | |||
}); | |||
} | |||
return vow.promise; | |||
}, | |||
kept: function (value) { | |||
// Returns a new kept promise. | |||
var vow = VOW.make(); | |||
vow.keep(value); | |||
return vow.promise; | |||
}, | |||
broken: function (reason) { | |||
// Returns a new broken promise. | |||
var vow = VOW.make(); | |||
vow.break(reason); | |||
return vow.promise; | |||
} | |||
}; | |||
}()); | |||
</nowiki> | |||
==Конструктор промисов== | |||
[] | |||
==Дополнительная информация== | |||
*[http://functionaljavascript.blogspot.ru/ Functional JavaScript (блог)] | |||
*[https://rsdn.ru/forum/decl/4047805.all Что такое монады? (форум)] | |||
*[http://jabberwocky.eu/2012/11/02/monads-for-dummies/ Monads for Dummies] | |||
*[https://curiosity-driven.org/monads-in-javascript Monads in JavaScript] | |||
*[https://blog.jcoglan.com/2011/03/05/translation-from-haskell-to-javascript-of-selected-portions-of-the-best-introduction-to-monads-ive-ever-read/ Translation from Haskell to JavaScript of selected portions of the best introduction to monads I’ve ever read] | |||
*[https://blog.jcoglan.com/2011/03/11/promises-are-the-monad-of-asynchronous-programming/ Promises are the monad of asynchronous programming] | |||
*[http://habrahabr.ru/post/247765/ Цикл статей о теории категорий] | |||
===Чистые и Грязные функции=== | |||
То, что мы называем функциями в C++ или любом другом императивном языке, не то же самое, что математики называют функциями. Математическая функция — просто отображение значений в значения. | |||
Мы можем реализовать математическую функцию на языке программирования: такая функция, имея входное значение будет рассчитать выходное значение. Функция для получения квадрата числа, вероятно, умножит входное значение само на себя. Она будет делать это при каждом вызове, и гарантированно произведет одинаковый результат каждый раз, когда она вызывается с одним и тем же аргументом. Квадрат числа не меняется с фазами Луны. | |||
Кроме того, вычисление квадрата числа не должно иметь побочного эффекта, вроде выдачи вкусного ништячка вашей собаке. «Функция», которая это делает, не может быть легко смоделирована математической функцей. | |||
В языках программирования функции, которые всегда дают одинаковый результат на одинаковых аргументах и не имеют побочных эффектов, называются чистыми. В чистом функциональном языке, наподобие Haskell, все функции чисты. Благодаря этому проще определить денотационную семантику этих языков и моделировать их с помощью теории категорий. Что касается других языков, то всегда можно ограничить себя чистым подмножеством, или размышлять о побочных эффектах отдельно. Позже мы увидим, как монады позволяют моделировать все виды эффектов, используя только чистые функции. В итоге мы ничего не теряем, ограничиваясь математическими функциями. | |||
=== * === | |||
С точки зрения программиста '''монада''' - это абстрактный контейнер с тремя функциями. | С точки зрения программиста '''монада''' - это абстрактный контейнер с тремя функциями. |
Текущая версия от 11:49, 12 июня 2015
Вместо вступления
Для человека, привыкшему к императивным языкам, испорченному годами объектно-ориентированного мышления монады кажутся странными. Монады являются и одновременно не являются контейнерами. Они — обёртки, упаковки для вычислений, а не для значений. Однако они обёртывают вычисления не для того, чтобы их было удобнее хранить в монадных коробочках, а чтобы их можно было удобнее соединять друг с другом. Вместо «коробочек» представьте обыкновенные кирпичи, которые ровно кладутся друг к другу. Это, кстати, похоже на шаблон Adapter в ОО-проектировании. Каждая монада определяет какой-то способ передавать результат от одного вычисления к другому и реализует стандартный интерфейс. И что бы ни случилось, результат всегда останется в той же монаде (даже, если произойдёт сбой, fail).
Самая простая программистская аналогия монадам, которую я придумал, это конвееры (pipes) в командной оболочке Unix. Монады обеспечивают однонаправленный конвеер для вычислений. То же самое делают и конвееры в Unix. Например:
$ seq 1 5 | awk '{print $1 " " $1*$1*$1 ;}' 1 12 83 274 645 125
seq создаёт список целых чисел. awk вычислят куб каждого из них. Что здесь замечательного? У нас есть две слабо связанные друг с другом программы, которым мы можем легко указать работать вместе. Поток текста создаваемый программой слева попадает по конвееру в программу справа, которая может читать этот поток, что-то с ним делать и создавать уже новый текстовый поток. Текстовый поток — общий формат для результата вычислений, | — операция, связывающая их воедино.
Применение:
- MayBe -привязка точной информации об ошибке к оборачиваемой функции;
- Continuation - связывание нескольких функций между собой
- Writer - привязка текстовой информации к функции, например логгинга
- I/O - спросить пользователя, дождаться ответа из терминала, отреагировать на ответ; или прочитать файл, дождаться когда он будет доступен, прочитать содержимое, закрыть файл;
- Identity - привязка/подмена информации в возвращаемом значении;
- State - привязка состояния к функции
- другие (смотрите ссылки в русской статье на википедии и раздел «Ссылки» статьи на английском).
Парадигмы (стили) программирования
Языки программирования состоят из операторов, условных операторов, операторов цикла и функций. Наличие условных операторов и операторов циклов являются отличительными чертами "императивных языков программирования". Функциональные языки, как правило, поддерживают только операторы и функции.
Интересно, что ни один из трех языков, Java, C ++ и C, не являются функциональными языками программирования.Язык C - императивный язык программирования, C ++ и Java императивно / объектно- ориентированные языки программирования. Т.е. существуют три парадигмы (стиля) программирования: императивный, объектно-ориентированный и функциональный. Существует еще один, декларативный стиль.
Различия между этими парадигмами заложены в основе. Императивное и объектно-ориентированное программирование основаны на «машине Тьюринга». Функциональное программирование на базе "лямбда-исчисления", а декларативное программирование основано на «логике первого порядка". Будут рассмотрены различия между, императивным, объектно-ориентированным и функциональном программировании на практическом уровне.
В императивном языке программирования изменение состояния программы достигается путем выполнения серии операторов и контролирует поток, прежде всего, с помощью условных операторов, операторов цикла и вызовов функций. Программа, приведенная ниже, простая реализация метода JavaScript Array.join в императивном стиле.
function simpleJoin(stringArray) { var accumulator = ''; for (var i=0, l=stringArray.length; i < l; i++) { accumulator = accumulator + stringArray[i]; } return accumulator; }
Код выше - последовательный. Мы перебераем массив и добавляем каждый элемент в строку-аккумулятор и возвращаем её. Сейчас мы перепишем эту функцию в объектно-ориентированном способом. Так как JavaScript имеет класс Array, мы добавим этот метод для класса Array, так что каждый экземпляр этого класса получит доступ к этой функции. JavaScript использует наследование через прототипы и поэтому мы добавляем эту функцию в прототип класса Array.
Array.prototype.simpleJoin = function() { var accumulator = ""; for (var i=0, l=this.length; i < l; i++) { accumulator = accumulator + this[i]; } return accumulator; }
Объектно-ориентированный вариант похож на императивную версию, кроме того, что функция теперь метод класса. Объектно-ориентированные языки, как правило, императивные также.
Теперь запишем функциональную версию этой функции.
function simpleJoin(stringArray, i, accumulator) { if (i === stringArray.length) { return accumulator; } else { return simpleJoin(stringArray, i+1, accumulator+stringArray[i]) } }
Первое, что нужно отметить, это то, что для итерации не используется цикл . Вместо этого для итерации используется рекурсия. Действительно, это одна из характеристик функционального языка программирования.
При первом вызове функции для массива stringArray: i установлено в 0 и accumulator установлен в "". Второй раз функции вызывается из себя с тем же stringArray, i установлен на i + 1 и accumulator равен accumulator + StringArray [i]. И мы продолжаем, пока i === stringArray.length, тогда мы вернем accumulator. Мы будем обсуждать рекурсию подробно позже. Просто помните, здесь использовалась рекурсия для итерации.
Но осталось кое-что еще от императивного стиля - условный оператор. Функциональные языки, как правило, используют выражения вычисляющие какое-либо значение, вместо выражений, которые ничего не вычисляют. Итак, давайте перепишем функцию, чтобы сделать его как функциональные, как можно в JavaScript.
function simpleJoin(stringArray, i, accumulator) { return (i === stringArray.length) ? accumulator : simpleJoin(stringArray, i + 1, accumulator + stringArray[i]) }
Теперь это функциональный стиль, как его можно написать с помощью JavaScript. Вместо того, чтобы, возвращать значение на основе вычислений условного оператора, мы возвращаем значение вычисляемое условным оператором. Значение первого выражения возвращается, если верно и второе, если ложно.
Мы видим, что функциональная версия короче. Действительно, одним из преимуществ функционального программирования является то, что необходимо меньшей кода для того что бы сделать то же самое, что приводит к лучшей читаемости и ремонтопригодности.
Однако в случае JavaScript, как здесь, нельзя использовать рекурсию для итерации(прим. хвостовые рекурсии не поддерживает язык). Вы должны продолжать использовать императивный или объектно-ориентированного метода для итерации. Это потому, что JavaScript (пока) не поддерживает "оптимизацию хвостового вызова". Мы будем обсуждать хвостовую рекурсию, и хвостовую оптимизацию, и как обойти эту проблему позже. JavaScript - мультипарадигмальный язык программирования:императивный, объектно-ориентированный и функциональный язык.
Хорошим примером мультипарадигмальной природы JavaScript является метод Array.forEach. Обратите внимание, что все современные браузеры уже реализовали это. Вот простая реализация.
Array.prototype.forEach = function(callback) { for (var i = 0, len = this.length; i < len; ++i) { callback(this[i], i, this); } }
В этом коде цикл for императивный код. Добавление в прототип объектно-ориентированное. Передача функции в качестве аргумента другой функции (callback) - функциональный код и эта возможность функционального программирования известна как “higher order function” (функции высшего порядка). В JavaScript, мы принимаем это как должное - передача функции в качестве аргумента. Удивительно этой возможности не было в самых популярных языках, до недавнего времени. например, вы не можете передавать функции в качестве аргументов в Java, хотя вы можете сделать это косвенно, через интерфейсы. То же самое в случае с C, хотя вы можете сделать это косвенно, используя указатели.
Функции первого класса и замыкания
Возможность функционального программирования, реализованые Бренданом Эйчом в JavaScript были first class functions или first class citizens. Это означает что функции рассматриваются как и все другие переменные. Т.е. можно передать их в качестве аргументов функций, вы можете вернуть их в качестве значений от других функций, или вы можете назначить им переменные или структуры данных. Мы видели ранее передачу функции в качестве аргумента. Вот пример присвоения функции переменной.
function greet(name) { console.log("Hello " + name); } greet("John"); // "Hello John" var sayHello = greet; sayHello("Alex"); // "Hello Alex"
Некоторые теоретики языка программирования рассматривают "анонимные функции" как функции первого класса. Чтобы не отстать, Брендан Эйч выбросил анонимные функции в миксе. Вот анонимная функция в JavaScript.
function(name) { console.log(“Hello “ + name); }
Если Вы заметили, мы не давали этой функции имя. Ведь это анонимная функция. Если вы попытаетесь запустить код, указанный выше, вы получите сообщение об ошибке. Что-то на подобие "вы не можете запустить код в этом контексте". И это правильно. Они могут быть назначены только на что-то, или передаться в качестве аргументов функции.
var sayHello = function(name) { console.log(“Hello “ + name); } sayHello("Jane"); // "Hello Jane"
Что делать, если мы хотим изменить приветствие? Иногда мы хотели бы сказать, "Hi", а не "Hello". Мы могли бы создать обобщенную функцию "createGreeting", которая, в свою очередь "сочинит" еще одну функцию для вас, и вернет новую комбинированную функцию. Так что, если мы бы хотели сказать "Hi" мы вернули функцию, а если мы хотели сказать "Hello" мы бы вернули другую функцию, которая говорит "Hello". Мы можем делать все это потому что JavaScript поддерживает функции первого класса, и мы можем вернуть функцию из другой функций. Вот код.
function createGreeting(greeting) { return function(name) { console.log(greeting + " " + name); } } var sayHi = createGreeting("Hi"); sayHi("Jack"); // "Hi Jack" var sayHello = createGreeting("Hello"); sayHello("Jack"); // "Hello Jack"
Функция createGreeting принимает greeting в качестве аргумента. Функция возвращает новую анонимную функцию. Однако созданная анонимная функция была создана внутри другой функции createGreeting. Таким образом она является вложенной функцией. Теперь, поскольку наш язык поддерживает анонимные функции, он также будет поддерживать вложенные функции. И когда мы возвращаемся вложенные функции из нашей функции мы столкнуться с другой проблемой.
Анонимная функция принимает аргумент name и печатает в консоли greeting+name.Переменная name является аргументом анонимной функции, и ведет себя так же, как и любой другой переменной, определенной в функции. Другими словами name "локальна" для анонимной функции. Но это не относится к переменной greeting. Она определен в другой функции - createGreeting и, следовательно, "не локальна" для анонимной функции. Однако анонимные функции могут получить доступ к переменной greeting и это называется Лексическая область видимости.
“Scope” (контекст, граница) переменной является её "видимостью" в пределах программы. "Лексическая область" означает, что видимость распространяется для всего текста (кода). Поэтому можно сказать “локальная переменная лексически ограничена” внутри функции, это значит, что локальные переменные функции видны для всего текста внутри функции, даже для кода внутри другой вложенной функции. Это также означает, что когда вы запускаете вложенную функцию вне окружения лексического контекста, the nested functions non local variable will not be visible. И в этом проблема возвращения вложенных функций из другой функции. И в самом деле вот что мы здесь делаем.
var sayHi = createGreeting("Hi");
В строке выше мы присваиваем возвращенную анонимную функцию переменной SayHi. И вызываем функцию в следующей строке.
sayHi(“Jack”)
Функция sayHi вызывается вне createGreeting. И переменная greeting недоступна вне createGreeting. Переменные доступные в контексте в котором они были определены, могут быть недоступны в контексте в котором они действительно вызываются. Вот почему языки, такие как C не поддерживает вложенные функции. Для того, чтобы так работать, язык должен поддерживать другой возможность функционального программирования под названием замыкание. JavaScript поддерживает замыкания. Любой язык, который поддерживает функции первого класса и вложенные функции должен поддерживать замыкания.
Замыкание является ссылкой на все не локальные переменные функции. В предыдущем примере greeting была не локальной переменной, а name локальной. Замыкание-это таблица ссылок на все используемые в функции не локальные переменные. Это позволяет функции по-прежнему обращаться к не локальным переменным, даже если функция выходит из контекста этих переменных.
Функторы
Рассмотрим функцию.
function plus1(value) { return value + 1 }
Это просто функция, которая принимает целое число и добавляет единицу к нему. Аналогично создадим другую функция Plus2. Мы будем использовать эти функции позже.
function plus2(value) { return value + 2 }
И мы могли бы написать обобщенную функцию, чтобы использовать любую из этих функций, как и когда это требуется.
function F(value, fn) { return fn(value) } F(1, plus1) ==>> 2
Эта функция будет работать хорошо, пока значение передается целое. Попробуем массив.
F([1, 2, 3], plus1) ==>> '1,2,31'
Ой. Мы взяли массив целых чисел, добавили целое и получили строку! Не только он это сделал неправильную вещь, мы получили строку передав массив. Другими словами наша программа "испачкала" структуру ввода. Мы хотим чтобы F работала "правильно". Правильно, это "сохранить структуру" на выходе.
Что значит "сохранить структуру"? Наша функция должна "развернуть" исходный массива и получить свои элементы. Затем вызвать переданную функцию для каждого элемента. Затем обернуть возвращаемые значения в новый массив и вернуть его. К счастью JavaScript имеет встроенную такую функцию - map.
[1, 2, 3].map(plus1) ==>> [2, 3, 4]
Функция map это функтор!
Функтор - это функция, получающая данные и функцию на входе и сохраняющая структуру данных на выходе.
Подробнее.
Функтор является функцией, получающей данные и функцию, разворачивает данные, чтобы получить его элементы, вызывает функцию к каждому элементу, оборачивает возвращаемые значения в новую структуру, и возвращает новую структуру.
Важно. В зависимости от типа данных, разворачивание может дать простые данные или структуру данных. Кроме того, возвращаемая структура не обязательно должна быть того же типа, что и исходная. В случае map и входные и возвращаемые данные имеют одинаковую структуру (массив). Возвращаемая структура может быть любого типа, так что бы можно было добраться до отдельных элементов структуры. Так что, если есть функция, которая принимает и массив и возвращает значение типа Object с массивом индексов в качестве ключей, и соответствующими значениями, также будут функтором.
В случае JavaScript, функция filter - функтор, потому что он возвращает массив, однако forEach не функтор, потому что он возвращает undefined, т.е.. forEach не поддерживает структуры.
Функторы пришли из теории категорий в математике, где функторы определяются как "гомоморфизм между категориями". Давайте расшифруем:
- homo = то же самое
- morphisms = функции, поддерживающие структуры
- category = тип
Согласно теории, функция F является функтор, когда для двух компонуемых обычных функции f и g выполняется равенство:
F(f . g) = F(f) . F(g),
где . (точка) обозначает композицию, т.е функторы должны сохранить композицию.
Поэтому, учитывая это уравнение можно проверить, является данная функция действительно функтором или нет.
Array Functor
Мы видели, что map это функтор, который действует по типу Array. Докажем, что JavaScript функция Array.map это функтор.
function compose(f, g) { return function(x) {return f(g(x))} }
Композиция функций выполняется из набора функций при помощи вызова следующей функции, с результатами предыдущей функции. Обратите внимание, что наша функция compose работает справа налево. g называется раньше, чем f.
[1, 2, 3].map(compose(plus1, plus2)) ==>> [ 4, 5, 6 ] [1, 2, 3].map(plus2).map(plus1) ==>> [ 4, 5, 6 ]
Да! Map действительно функтор.
Попробуем некоторые функторы. Вы можете написать функторы для значений любого типа, до тех пор, как вы можете развернуть значение и вернуть структуру.
String Functor
Можно ли написать функтор для типа строки? Можено развернуть строку? На самом деле можно, если думать о строке как о массиве символов. Все дело в том, как вы смотрите на значения. Также известно, что символы имеют текстовые коды, которые являются целыми числами. Таким образом, использовать plus1 для charcode символа, обернуть их обратно в строку, и вернуть её.
function stringFunctor(value, fn) { var chars = value.split("") return chars.map(function(char) { return String.fromCharCode(fn(char.charCodeAt(0))) }).join("") } stringFunctor("ABCD", plus1) ==>> "BCDE"
Вы можете начать видеть, как масштабны функторы. Вы можете фактически написать парсер, используя String Functor в качестве основы.
Function Functor
В JavaScript функции являются first class citizens. Это означает, что вы можете обращаться к функции, как к любой другому значению. Можно ли написать функтор для значения типа функции? Да! Но как мы развернуть функцию? Функцию можно развернуть, вызвав её и получив возвращаемое ей значение. Но мы сразу столкнемся с проблемой. Для вызова функции мы должны передать аргументы. Помните, что этот функтор принимает только функцию в качестве аргумета. Мы можем решить эту проблему, вернув новую (анонимную) функцию. Новая (анонимная) функция будет вызвана с аргументами (initial) и мы, в свою очередь вернем анонимную функцию (без аргументов), в которой вызовем оригинальную функцию-аргумент (fn) в качестве параметров которой будет результат первой анонимной функции. (ЖЕСТЬ)
function functionFunctor(value, fn) { return function(initial) { return function() { return fn(value(initial)) } } } var init = functionFunctor(function(x) {return x * x}, plus1) var final = init(2) final() ==> 5
Наша Function Functor действительно ничего не делает. Но есть несколько моментов. Ничего не происходит, пока вы не вызовете final. Каждая функция находится в состоянии анабиоза, пока вы не вызовете финал. Function Functor формирует основу для более удивительным функциональной вещи как поддержание состояния, продолжение вызова и даже promises. Вы можете написать свои собственные Function Functor, чтобы сделать эти вещи!
MayBe Functor
function mayBe(value, fn) { return value === null || value === undefined ? value : fn(value) }
Да, это правильный функтор.
mayBe(undefined, compose(plus1, plus2)) ==>> undefined mayBe(mayBe(undefined, plus2), plus1) ==>> undefined mayBe(1, compose(plus1, plus2)) ==>> 4 mayBe(mayBe(1, plus2), plus1) ==>> 4
Итак mayBe проходит тест функтора. Здесь нет необходимости в разворачивании или оборачивании. Он просто возвращает ничего для ничего. mayBe полезно в качестве короткого замыкания, функции, которые можно использовать в качестве заменителя кода:
if (result === null) { return null } else { doSomething(result) }
Identity Function
function id(x) { return x }
Функция выше, что известно как функции идентификации. Это просто функция, которая возвращает значение, переданное ему. Он называется так для тождественности в математическом аппарате.
Мы узнали, что ранее функторы должны сохранить структуру. Однако то, что я не упомянул то, что функторы должны сохранить идентичность. т.е.
F(value, id) = value
Попробуем для map.
[1, 2, 3].map(id) ==>> [ 1, 2, 3 ]
Типы сигнатур
Тип сигнатуры функции это тип аргумента и возвращаемого значения. Тип сигнатуры для нашей plus1:
f: int -> int
Тип сигнатуры для map зависит от сигнатуры функции-аргумента. Так если map будет вызвана с функцией plus1, то её сигнатура:
map: [int] -> [int]
Но типы сигнатур Однако тип сигнатур функции-аргумента не обязательно должны быть такими же, как выше. Могут быть такие функции:
f: int -> string
из-за которой тип сигнатуры map бутет таким:
map: [int] -> [string]
Единственное ограничение в том, что изменение типа не влияет на композицию функтора. Таким образом, в общем тип сигнатуры функтора может быть таким:
F: A -> B
Другими словами map может принимать массив целых чисел и возвращть массив строк и это будет по-прежнему функтор.
Монады особый случай функторов чей тип сигнатуры
M: A -> A
Монады
Рассмотрим map functor из последней главы. Мы могли бы использовать map, чтобы перебрать два массивы добавление каждого элемента первого ко второму.
var result = [1, 2].map(function(i) { return [3, 4].map(function(j) { return i + j }) }) console.log(result) ==>> [ [ 4, 5 ], [ 5, 6 ] ]
Тип сигнатуры внутренней функции:
f: int -> int
и тип сигнатуры внутреннего map:
map: [int] -> [int]
Тип сигнатуры внешней функции:
f: int -> [int]
и тип сигнатуры внешнего map:
map: [int] -> [[int]]
Это правильное поведение функтора. Но это не то, что мы хотим. Мы хотим, чтобы результат был плоским, как показано ниже.
[ 4, 5, 5, 6 ]
Array Monad
Для того чтобы это произошло, тип сигнатуры функтора всегда должен быть ограничен
F: [int] -> [int]
Но функторы не предоставляют такое ограничение. Но монады да. Тип сигнатуры Array Monad:
M: [T] -> [T]
где T переданный тип. Вот почему map функтор, но не монада. Это еще не все. Мы должны поставить некоторые ограничения на тип данных, передаваемых в функцию. Функция не может возвращать любой тип. Мы можем решить эту проблему путем ограничения функции на возврат только типа массив. Так тип сигнатуры функции ограничен
f: T -> [T]
Эта функция известна как lift, потому что она поднимает тип до требуемого. Это также известно как монадическая функция (mf). И исходное значение переданное монаде называется монадическим значением (mv). (Прим. т.е. аргументом mv для монады всегда массив, результат - тоже массив, причем строго того же типа что и аргумент mv.) Вот arrayMonad.
function arrayMonad(mv, mf) { var result = [] mv.forEach(function(v) { result = result.concat(mf(v)) }) return result }
Теперь можно использовать Array Monad для выполнения первого вычисления
console.log(arrayMonad([1,2,3], function(i) { return [i + 1] })) ==>> [ 2, 3, 4 ]
Обратите внимание, что наша монадическая функция заворачивает результат в массив [i + 1]. Теперь давайте попробуем для с двумерной задачи с которой мы начали.
var result = arrayMonad([1, 2], function(i) { return arrayMonad([3, 4], function(j) { return [i + j] }) }) console.log(result) ==>> [ 4, 5, 5, 6 ]
Теперь заметно превосходство монад над функторами.
Мы можем написать общий двумерный итератор для массивов, которые примут два массива и функцию обратного вызова и применят её для каждого элемента обоих массивов.
function forEach2d(array1, array2, callback) { return arrayMonad(array1, function(i) { return arrayMonad(array2, function(j) { return [callback(i,j)] }) }) }
И мы можем попробовать эту функцию
forEach2d([1, 2], [3,4], function(i, j) { return i + j }) ==>> [ 4, 5, 5, 6 ]
Обратите внимание, что функция обратного вызова просто обычная функция, так что нам пришлось поднять(lift) её возвращаемые значения [callback(i,j)] в массив. Однако во всех монадах определена функция, чтобы сделать подъем. Её называют mResult. Добавим mResult к объекту функции arrayMonad. Функция concat не эффективна, поскольку она создает новый массив каждый раз. Взамен будем использовать array push. Вот окончательный код для Array Monad.
function arrayMonad(mv, mf) { var result = [] mv.forEach(function(v) { Array.prototype.push.apply(result, mf(v)) }) return result } arrayMonad.mResult = function(v) { return [v] }
и переписанная forEach2d
function forEach2d(array1, array2, callback) { return arrayMonad(array1, function(i) { return arrayMonad(array2, function(j) { return arrayMonad.mResult(callback(i,j)) }) }) }
Как упражнение попробуйте реализовать forEach3d.
ArrayMonad представляет одноместную функцию и иначе известный как bind или mbind. Для того, что бы функция стала монадой, необходимо определить по крайней мере функции mbind и mresult.
Identity Monad
Identity monad простейшая из всех монад, названная так потому, что это является идентичной mresult.
function indentityMonad(mv, mf) { return mf(mv) } identityMonad.mResult = function(v) { return v }
Она не очень полезная, но она правильная.
Maybe Monad
Maybe Monad подобна identity monad, за исключением того, что он не будет вызывать монадическую функцию для значений null или undefined. На самом деле она сводится к функтору mayBe.
function mayBeMonad(mv, mf) { return mv === null || mv === undefined || mv === false ? null : mf(mv) } mayBeMonad.mResult = function(v) { return v }
Законы монад
Первый закон монад
M(mResult(x), mf) = mf(x)
Который означает, что mResult должен сделать с x, чтобы превратить х в монадическое значение. М будет разворачивать это монадическое значение перед его применением в монадической функции mf. Давайте проверить это на нашем array monad.
var x = 4; function mf(x) { return [x * 2] } arrayMonad(arrayMonad.mResult(x), mf) ==>> [ 8 ] mf(x) ==>> [ 8 ]
Второй закон монад
M(mv, mResult) = mv
Это означает, что mBind извлекает значение mv ,а mResult должен будет обернуть назад это значение обратно монадическое. Это гарантирует, что mResult является одноместной функцией. Давайте проверить его. Это эквивалентно сохранению идентичности в случае функтора.
arrayMonad([1, 2, 3], arrayMonad.mResult) ==>> [ 1, 2, 3 ]
Третий закон монад
M(M(mv, mf), mg)) = M(mv, function(x){return M(mf(x), mg)})
Не имеет значения, если вы применяете mf к mv, а затем к mg, или применять mv к монадической функции, представляющей композицию mf и mg.
function mg(x) { return [x * 3] } arrayMonad(arrayMonad([1, 2, 3], mf), mg) ==>> [ 6, 12, 18 ] arrayMonad([1, 2, 3], function(x) { return arrayMonad(mf(x), mg) }) ==>> [ 6, 12, 18 ]
doMonad
Мы знаем, что монадическая функция принимает значение и возвращает монадическое значение. Монада принимает монадическое значение и монадическую функцию и возвращает монадическое значение. Что делать, если монадическая функция вызывает монаду с монадическим значением и самой себя, и возвращает результат? Это было бы правильная монадическая функция, потому что она возвращает монадическое значение.
Функция doMonad делает именно это. Это передает в монаду массив монадических значений и callback в качестве аргументов. Это определяет монадическую функцию как рекурсивно вызывающую монаду для каждого монадического значения и саму себя. Цикл заканчивается, когда не остается монадических значений. Она возвращает callback с каждым развернутым значением монадических значений.Сallback cb каррируется в замыкании, называемой оберткой и становится видимой для mf. Каррирование или карринг (англ. currying) в информатике — преобразование функции от многих аргументов в функцию, берущую свои аргументы по одному.
function curry(fn, numArgs) { numArgs = numArgs || fn.length return function f(saved_args) { return function() { var args = saved_args.concat(Array.prototype.slice.call(arguments)) return args.length === numArgs ? fn.apply(null, args) : f(args) } }([]) } function doMonad(monad, values, cb) { function wrap(curriedCb, index) { return function mf(v) { return (index === values.length - 1) ? monad.mResult(curriedCb(v)) : monad(values[index + 1], wrap(curriedCb(v), index + 1)) } } return monad(values[0], wrap(curry(cb), 0)) } doMonad(arrayMonad, [[1, 2], [3, 4]], function(x, y) { return x + y }) //==>> [ 4, 5, 5, 6 ]
Теперь нет необходимости в функции forEach2d , которую мы написали ранее! И лучшее еще впереди!
Пример использования doMonad для массивов
Мы можем написать общую функцию FOR для массива, которая принимает набор массивов и обратный вызов для аргументов.
function FOR() { var args = [].slice.call(arguments) callback = args.pop() return doMonad(arrayMonad, args, callback) } FOR([1, 2], [3, 4], function(x, y) { return x + y }) //==>> [ 4, 5, 5, 6 ] FOR([1, 2], [3, 4], [5, 6], function(x, y, z) { return x + y + z }) //==>> [ 9, 10, 10, 11, 10, 11, 11, 12 ]
Удивительно!
State Monad
Ранее рассматривался function fucntor, который принимал значения типа function. Аналогично монадические значения могут также быть функциями. Однако важно различать монадические функции и монадические значения, являющиеся функциями. Тип сигнатуры монадической функции
mf: v -> mv
т.е. принимает значение и поднимает его до монадического значения. Вспомним, что монадическое значение само по себе функция. Поэтому mf вернет функцию mv.
Типы сигнатуры монадического значения, являющегося функцией, зависит от того, что делает сама эта функция. В случаеs state monad тип сигнатуры монадического значения:
mv: state -> [value, new state]
Функция в монадическом значении принимает состояние state и возвращает массив, содержащий данные value и новое состояние new state. Состояние state может быть любого типа: array, string, integer и т.д.
StateMonad принимает монадическое значение и монадическое функцию и возвращает функцию, для которой мы должны задать исходное состояние. Исходное состояние передается mv, которое возвращает значение. Затем вызывается mf с этим значением и mf возвращает монадическое значение, которое является функцией. Мы должны вызвать эту функцию с NewState. Уф!
function stateMonad(mv, mf) { return function(state) { var compute = mv(state) var value = compute[0] var newState = compute[1] return mf(value)(newState) } }
mResult для state monad:
stateMonad.mResult = function(value) { return function(state) { return [value, state]; } }
(Примечание о применении: проверка на ошибку)
Parser Monad
Парсер- это функция, которая принимает строку, анализирует её по некоторым критериям и возвращает совпавшую часть и остаток. Позволяет написать тип сигнатуры функции.
parser: string -> [match, newstring]
Это выглядит как монадическое значение state monad, с ограничением типа state строкой. Но это еще не все, анализатор вернется NULL, если строка не соответствует этим критериям. Так что давайте напишем Parser Monad, чтобы удивить разницу.
function parserMonad(mv, mf) { return function(str) { var compute = mv(str) if (compute === null) { return null } else { return mf(compute[0])(compute[1]) } } } parserMonad.mResult = function(value) { return function(str) { return [value, str]; } }
Как мы видели ранее, монады требует, чтобы вы определить по крайней мере две функции, mBind (саму функцию монаду) и mResult. Но это еще не все. При желании вы можете определить еще две функции, mZero и Mplus.
mZero является определение «Nothing» для монады. например. для arrayMonad, mZero будет [ ]. В случае анализатора монады mZero определяется следующим образом. (mZero должен иметь тот же тип сигнатуры монадического значения).
parserMonad.mZero = function(str) { return null }
Mplus это функция, которая принимает монадические значения в качестве аргументов, и игнорирует mZero среди них. Как принятые значения обрабатываются зависит от каждой монады. Для parser monad, mZero будет взять набор анализаторов (монадические значения Parser монады) и вернет значение, возвращенное первым парсером который вернет значение не mZero (NULL).
parserMonad.mPlus = function() { var parsers = Array.prototype.slice.call(arguments) return function(str) { var result, i for (i = 0; i < parsers.length; ++i) { result = parsers[i](str) if (result !== null) { break; } } return result } }
Continuation Monad
Continuation monad проста для понимания. В задании о композиции функций была рассмотрена композиция двух функций f и g:
(f . g) = f(g(x))
Функция f называется продолжением функции g.
Также известно, что можно обернуть значение в функцию, создав замыкание. В примере ниже, вложенная имеет доступ к value, обернутому в замыкание.
function(value) { return function() { // value can be accessed here } }
Монадическое значение continuation monad является функцией, которая возвращает функцию-продолжение, вызывая её со значением из собственной "обёртки".
function(continuation) { return continuation(value) }
Функция mResult этой монады принимает простое значение и "поднимает" его до монадического значения. Так будет выглядеть функция mResult для continuation monad.
var mResult = function(value) { return function(continuation) { return continuation(value) } }
Итак mResult это функция, которая принимает простое значение и возвращает монадическое значение, которое будет передано в функцию-продолжение.
Сама continuation monad или mBind более сложная.
var continuationMonad = function(mv, mf) { return function(continuation) { // we will add to here next } }
Первым вызовом возвращается функция, которую нужно вызвать с функцией-продолжением. Это просто. Но как развернуть value внутри mv? mv принимает функцию-продолжение , но вызов mv с функцией-продолжением не происходит. Мы должны развернуть значение в mv и вызвать mf первым. Таким образом, мы должны обмануть mv в передаче нам значение путем вызова его с нашей собственной функцией-продолжения.
mv(function(value) { // gotcha! the value })
Добавим эту функцию в код
var continuationMonad = function(mv, mf) { return function(continuation) { return mv(function(value) { // gotcha! the value }) } }
Теперь все что мы должны сделать - это вызвать mf со значением. Мы знаем, что монадическая функция принимает значение и возвращает монадическое значение. Так мы называем возвращаемое монадическое значение из функции mf с продолжением. Уф! Вот полный код для продолжения монады.
var continuationMonad = function(mv, mf) { return function(continuation) { return mv(function(value) { return mf(value)(continuation) }) } } continuationMonad.mResult = function(value) { return function(continuation) { return continuation(value) } }
Библиотека
Код
/* monadjs Monad Library for JavaScript Copyright (c) 2013 Santosh Rajan License - MIT - https://github.com/santoshrajan/monadjs/blob/master/LICENSE */ exports.version = "0.1.0" // Curry function function curry(fn, numArgs) { numArgs = numArgs || fn.length return function f(saved_args) { return function() { var args = saved_args.concat(Array.prototype.slice.call(arguments)) return args.length === numArgs ? fn.apply(null, args) : f(args) } }([]) } // The identity Monad function identityMonad(mv, mf) { return mf(mv) } identityMonad.mResult = function(v) { return v } // The mayBe Monad function mayBeMonad(mv, mf) { return mv === null || mv === undefined || mv === false ? null : mf(mv) } mayBeMonad.mResult = function(v) { return v } // The array Monad function arrayMonad(mv, mf) { var result = [] mv.forEach(function(v) { Array.prototype.push.apply(result, mf(v)) }) return result } arrayMonad.mResult = function(v) { return [v] } // The state Monad function stateMonad(mv, mf) { return function(state) { var compute = mv(state) return mf(compute[0])(compute[1]) } } stateMonad.mResult = function(value) { return function(state) { return [value, state]; } } // The parser Monad function parserMonad(mv, mf) { return function(str) { var compute = mv(str) if (compute === null) { return null } else { return mf(compute[0])(compute[1]) } } } parserMonad.mResult = function(value) { return function(str) { return [value, str]; } } parserMonad.mZero = function(str) { return null } parserMonad.mPlus = function() { var parsers = Array.prototype.slice.call(arguments) return function(str) { var result, i for (i = 0; i < parsers.length; ++i) { result = parsers[i](str) if (result !== null) { break; } } return result } } // The continuation Monad function continuationMonad(mv, mf) { return function(continuation) { return mv(function(value) { return mf(value)(continuation); }) } } continuationMonad.mResult = function(value) { return function(continuation) { return continuation(value) } } function doMonad(monad, values, cb) { function wrap(curriedCb, index) { return function mf(v) { return (index === values.length - 1) ? monad.mResult(curriedCb(v)) : monad(values[index + 1], wrap(curriedCb(v), index + 1)) } } return monad(values[0], wrap(curry(cb), 0)) } exports.identity = identityMonad exports.mayBe = mayBeMonad exports.array = arrayMonad exports.state = stateMonad exports.parser = parserMonad exports.continuation = continuationMonad exports.do = doMonad
Примеры
arrayMonad.js
var monads = require("monadjs"); function forEach3D(iArray, jArray, kArray, callback) { return monads.do(monads.array, [iArray, jArray, kArray], callback) } var result = forEach3D([1, 2], [3, 4], [5, 6], function(i, j, k) { return i + j + k }) console.log(result)
identitymonad.js
var monads = require("monadjs") var result = monads.do(monads.identity, [1, 2], function(a, b) { return a + b }) console.log(result);
maybemonad.js
var monads = require("monadjs"); var push = function(element) { return function(state) { var newstate = [element] return [undefined, newstate.concat(state)] } } var pop = function() { return function(state) { var newstate = state.slice(1) return [state[0], newstate] } } var result = monads.do(monads.state, [ push(5), push(10), push(20), pop() ], function(val1, val2, val3, val4) { return val4 } ) console.log(result([]))
statemonad.js
var monads = require("monadjs"); var push = function(element) { return function(state) { var newstate = [element] return [undefined, newstate.concat(state)] } } var pop = function() { return function(state) { var newstate = state.slice(1) return [state[0], newstate] } } var result = monads.do(monads.state, [ push(5), push(10), push(20), pop() ], function(val1, val2, val3, val4) { return val4 } ) console.log(result([]))
Конструктор монад
// monad.js // Douglas Crockford // 2015-05-02 // Public Domain // The MONAD function is a macroid that produces monad constructor functions. // It can take an optional modifier function, which is a function that is // allowed to modify new monads at the end of the construction processes. // A monad constructor (sometimes called 'unit' or 'return' in some mythologies) // comes with three methods, lift, lift_value, and method, all of which can add // methods and properties to the monad's prototype. // A monad has a 'bind' method that takes a function that receives a value and // is usually expected to return a monad. // var identity = MONAD(); // var monad = identity("Hello world."); // monad.bind(alert); // var ajax = MONAD() // .lift('alert', alert); // var monad = ajax("Hello world."); // monad.alert(); // var maybe = MONAD(function (monad, value) { // if (value === null || value === undefined) { // monad.is_null = true; // monad.bind = function () { // return monad; // }; // return null; // } // return value; // }); // var monad = maybe(null); // monad.bind(alert); // Nothing happens. /*jslint this */ function MONAD(modifier) { 'use strict'; // Each unit constructor has a monad prototype. The prototype will contain an // is_monad property for classification, as well as all inheritable methods. var prototype = Object.create(null); prototype.is_monad = true; // Each call to MONAD will produce a new unit constructor function. function unit(value) { // Construct a new monad. var monad = Object.create(prototype); // In some mythologies 'bind' is called 'pipe' or '>>='. // The bind method will deliver the unit's value parameter to a function. monad.bind = function (func, args) { // bind takes a function and an optional array of arguments. It calls that // function passing the monad's value and bind's optional array of args. // With ES6, this horrible return statement can be replaced with // return func(value, ...args); return func.apply( undefined, [value].concat(Array.prototype.slice.apply(args || [])) ); }; // If MONAD's modifier parameter is a function, then call it, passing the monad // and the value. if (typeof modifier === 'function') { value = modifier(monad, value); } // Return the shiny new monad. return monad; } unit.method = function (name, func) { // Add a method to the prototype. prototype[name] = func; return unit; }; unit.lift_value = function (name, func) { // Add a method to the prototype that calls bind with the func. This can be // used for ajax methods that return values other than monads. prototype[name] = function () { return this.bind(func, arguments); }; return unit; }; unit.lift = function (name, func) { // Add a method to the prototype that calls bind with the func. If the value // returned by the func is not a monad, then make a monad. prototype[name] = function () { var result = this.bind(func, arguments); return result && result.is_monad === true ? result : unit(result); }; return unit; }; return unit; }
Конструктор Promise
// vow.js // Douglas Crockford // 2015-05-02 // Public Domain /*global setImmediate */ var VOW = (function () { 'use strict'; // The VOW object contains a .make function that is used to make vows. // It may also contain other useful functions. // In some mythologies, 'VOW' is called 'deferrer'. function enlighten(queue, fate) { // enlighten is a helper function of herald and .when. It schedules the // processing of all of the resolution functions in either the keepers queue // or the breakers queue in later turns with the promise's fate. queue.forEach(function (func) { setImmediate(func, fate); }); } return { make: function make() { // The make function makes new vows. A vow contains a promise object and the // two resolution functions (break and keep) that determine the fate of the // promise. var breakers = [], // .when's broken queue fate, // The promise's ultimate value keepers = [], // .when's kept queue status = 'pending'; // 'broken', 'kept', or 'pending' function enqueue( resolution, // 'keep' or 'break' func, // A function that was registered with .when vow // A vow that provides the resolution functions ) { // enqueue is a helper function used by .when. It will append a function to // either the keepers queue or the breakers queue. var queue = resolution === 'keep' ? keepers : breakers; queue[queue.length] = typeof func !== 'function' // If func is not a function, push the resolver so that the value passes to // the next cascaded .when. ? vow[resolution] // If the func is a function, push a function that calls func with a value. // The result can be a promise, or not a promise, or an exception. : function (value) { try { var result = func(value); // If the result is a promise, then register our resolver with that promise. if (result && result.is_promise === true) { result.when(vow.keep, vow.break); // But if it is not a promise, then use the result to resolve our promise. } else { vow.keep(result); } // But if func throws an exception, then break our promise. } catch (e) { vow.break(e); } }; } function herald(state, value, queue) { // The herald function is a helper function of break and keep. // It seals the promise's fate, updates its status, enlightens // one of the queues, and empties both queues. if (status !== 'pending') { throw "overpromise"; } fate = value; status = state; enlighten(queue, fate); keepers.length = 0; breakers.length = 0; } // Construct and return the vow object. return { 'break': function (value) { // The break method breaks the promise. herald('broken', value, breakers); }, keep: function keep(value) { // The keep method keeps the promise. herald('kept', value, keepers); }, promise: { // The promise is an object with a .when method. is_promise: true, // The .when method is the promise monad's bind. The .when method can take two // optional functions. One of those functions may be called, depending on the // promise's resolution. Both could be called if the the kept function throws. when: function (kept, broken) { // Make a new vow. Return the new promise. var vow = make(); switch (status) { // If this promise is still pending, then enqueue both kept and broken. case 'pending': enqueue('keep', kept, vow); enqueue('break', broken, vow); break; // If the promise has already been kept, then enqueue only the kept function, // and enlighten it. case 'kept': enqueue('keep', kept, vow); enlighten(keepers, fate); break; // If the promise has already been broken, then enqueue only the broken // function, and enlighten it. case 'broken': enqueue('break', broken, vow); enlighten(breakers, fate); break; } return vow.promise; } } }; }, every: function every(array) { // The every function takes an array of promises and returns a promise that // will deliver an array of results only if every promise is kept. var remaining = array.length, results = [], vow = VOW.make(); if (!remaining) { vow.break(array); } else { array.forEach(function (promise, i) { promise.when(function (value) { results[i] = value; remaining -= 1; if (remaining === 0) { vow.keep(results); } }, function (reason) { remaining = NaN; vow.break(reason); }); }); } return vow.promise; }, first: function first(array) { // The first function takes an array of promises and returns a promise to // deliver the first observed kept promise, or a broken promise if all of // the promises are broken. var found = false, remaining = array.length, vow = VOW.make(); function check() { remaining -= 1; if (remaining === 0 && !found) { vow.break(); } } if (remaining === 0) { vow.break(array); } else { array.forEach(function (promise) { promise.when(function (value) { if (!found) { found = true; vow.keep(value); } check(); }, check); }); } return vow.promise; }, any: function any(array) { // The any function takes an array of promises and returns a promise that // will deliver a possibly sparse array of results of any kept promises. // The result will contain an undefined element for each broken promise. var remaining = array.length, results = [], vow = VOW.make(); function check() { remaining -= 1; if (remaining === 0) { vow.keep(results); } } // vow.js // Douglas Crockford // 2015-05-02 // Public Domain /*global setImmediate */ var VOW = (function () { 'use strict'; // The VOW object contains a .make function that is used to make vows. // It may also contain other useful functions. // In some mythologies, 'VOW' is called 'deferrer'. function enlighten(queue, fate) { // enlighten is a helper function of herald and .when. It schedules the // processing of all of the resolution functions in either the keepers queue // or the breakers queue in later turns with the promise's fate. queue.forEach(function (func) { setImmediate(func, fate); }); } return { make: function make() { // The make function makes new vows. A vow contains a promise object and the // two resolution functions (break and keep) that determine the fate of the // promise. var breakers = [], // .when's broken queue fate, // The promise's ultimate value keepers = [], // .when's kept queue status = 'pending'; // 'broken', 'kept', or 'pending' function enqueue( resolution, // 'keep' or 'break' func, // A function that was registered with .when vow // A vow that provides the resolution functions ) { // enqueue is a helper function used by .when. It will append a function to // either the keepers queue or the breakers queue. var queue = resolution === 'keep' ? keepers : breakers; queue[queue.length] = typeof func !== 'function' // If func is not a function, push the resolver so that the value passes to // the next cascaded .when. ? vow[resolution] // If the func is a function, push a function that calls func with a value. // The result can be a promise, or not a promise, or an exception. : function (value) { try { var result = func(value); // If the result is a promise, then register our resolver with that promise. if (result && result.is_promise === true) { result.when(vow.keep, vow.break); // But if it is not a promise, then use the result to resolve our promise. } else { vow.keep(result); } // But if func throws an exception, then break our promise. } catch (e) { vow.break(e); } }; } function herald(state, value, queue) { // The herald function is a helper function of break and keep. // It seals the promise's fate, updates its status, enlightens // one of the queues, and empties both queues. if (status !== 'pending') { throw "overpromise"; } fate = value; status = state; enlighten(queue, fate); keepers.length = 0; breakers.length = 0; } // Construct and return the vow object. return { 'break': function (value) { // The break method breaks the promise. herald('broken', value, breakers); }, keep: function keep(value) { // The keep method keeps the promise. herald('kept', value, keepers); }, promise: { // The promise is an object with a .when method. is_promise: true, // The .when method is the promise monad's bind. The .when method can take two // optional functions. One of those functions may be called, depending on the // promise's resolution. Both could be called if the the kept function throws. when: function (kept, broken) { // Make a new vow. Return the new promise. var vow = make(); switch (status) { // If this promise is still pending, then enqueue both kept and broken. case 'pending': enqueue('keep', kept, vow); enqueue('break', broken, vow); break; // If the promise has already been kept, then enqueue only the kept function, // and enlighten it. case 'kept': enqueue('keep', kept, vow); enlighten(keepers, fate); break; // If the promise has already been broken, then enqueue only the broken // function, and enlighten it. case 'broken': enqueue('break', broken, vow); enlighten(breakers, fate); break; } return vow.promise; } } }; }, every: function every(array) { // The every function takes an array of promises and returns a promise that // will deliver an array of results only if every promise is kept. var remaining = array.length, results = [], vow = VOW.make(); if (!remaining) { vow.break(array); } else { array.forEach(function (promise, i) { promise.when(function (value) { results[i] = value; remaining -= 1; if (remaining === 0) { vow.keep(results); } }, function (reason) { remaining = NaN; vow.break(reason); }); }); } return vow.promise; }, first: function first(array) { // The first function takes an array of promises and returns a promise to // deliver the first observed kept promise, or a broken promise if all of // the promises are broken. var found = false, remaining = array.length, vow = VOW.make(); function check() { remaining -= 1; if (remaining === 0 && !found) { vow.break(); } } if (remaining === 0) { vow.break(array); } else { array.forEach(function (promise) { promise.when(function (value) { if (!found) { found = true; vow.keep(value); } check(); }, check); }); } return vow.promise; }, any: function any(array) { // The any function takes an array of promises and returns a promise that // will deliver a possibly sparse array of results of any kept promises. // The result will contain an undefined element for each broken promise. var remaining = array.length, results = [], vow = VOW.make(); function check() { remaining -= 1; if (remaining === 0) { vow.keep(results); } } if (!remaining) { vow.keep(results); } else { array.forEach(function (promise, i) { promise.when(function (value) { results[i] = value; check(); }, check); }); } return vow.promise; }, kept: function (value) { // Returns a new kept promise. var vow = VOW.make(); vow.keep(value); return vow.promise; }, broken: function (reason) { // Returns a new broken promise. var vow = VOW.make(); vow.break(reason); return vow.promise; } }; }()); if (!remaining) { vow.keep(results); } else { array.forEach(function (promise, i) { promise.when(function (value) { results[i] = value; check(); }, check); }); } return vow.promise; }, kept: function (value) { // Returns a new kept promise. var vow = VOW.make(); vow.keep(value); return vow.promise; }, broken: function (reason) { // Returns a new broken promise. var vow = VOW.make(); vow.break(reason); return vow.promise; } }; }());
Конструктор промисов
[]
Дополнительная информация
- Functional JavaScript (блог)
- Что такое монады? (форум)
- Monads for Dummies
- Monads in JavaScript
- Translation from Haskell to JavaScript of selected portions of the best introduction to monads I’ve ever read
- Promises are the monad of asynchronous programming
- Цикл статей о теории категорий
Чистые и Грязные функции
То, что мы называем функциями в C++ или любом другом императивном языке, не то же самое, что математики называют функциями. Математическая функция — просто отображение значений в значения.
Мы можем реализовать математическую функцию на языке программирования: такая функция, имея входное значение будет рассчитать выходное значение. Функция для получения квадрата числа, вероятно, умножит входное значение само на себя. Она будет делать это при каждом вызове, и гарантированно произведет одинаковый результат каждый раз, когда она вызывается с одним и тем же аргументом. Квадрат числа не меняется с фазами Луны.
Кроме того, вычисление квадрата числа не должно иметь побочного эффекта, вроде выдачи вкусного ништячка вашей собаке. «Функция», которая это делает, не может быть легко смоделирована математической функцей.
В языках программирования функции, которые всегда дают одинаковый результат на одинаковых аргументах и не имеют побочных эффектов, называются чистыми. В чистом функциональном языке, наподобие Haskell, все функции чисты. Благодаря этому проще определить денотационную семантику этих языков и моделировать их с помощью теории категорий. Что касается других языков, то всегда можно ограничить себя чистым подмножеством, или размышлять о побочных эффектах отдельно. Позже мы увидим, как монады позволяют моделировать все виды эффектов, используя только чистые функции. В итоге мы ничего не теряем, ограничиваясь математическими функциями.
*
С точки зрения программиста монада - это абстрактный контейнер с тремя функциями.
- map — заменяет содержимое контейнера без изменения самого контейнера. Заменяем каждый гвоздь в коробке шурупом, каждый int в массиве float-ом — так map и работает.
- unit — берет элемент и возвращает контейнер с одним этим элементом. Делаем из гвоздя коробку с одним гвоздем. Делаем из int массив из одного int.
- join — уменьшает вложенность контейнеров — из коробки коробок гвоздей делает коробку с гвоздями (из массива массивов int-ов — массив int-ов). Ну или из коробки коробок коробок гвоздей делаем коробку коробок гвоздей. Это уже сложная концепция, доступная только программистам и более абстрактно развитым товарищам; обычный человек будет обескуражен тем, как в одну коробку могли поместиться несколько точно таких-же коробок. Впрочем, простая замена коробок коробок на мешки мешков или пакеты пакетов позволяет совершить абстрактно-теоретико-категориальный прорыв.
Монада - это интерфейс с двумя методами:
- "поднять в монаду". Давайте называть этот метод 'pure'. Функция от одного аргумента. На входе какое-то значение, на выходе это же значение, но помеченое другим типом.
- "применить функцию к значению в монаде" или "совершить действие". В энергичном языке, думаю, уместо было бы название 'apply'. Функция от двух аргументов. На входе монадическое значение (полученное из первой функции) и собственно функция-действие, которое нужно применить к первому аргументу. На выходе новое монадическое значение, то есть изменённое функцией-действитем. Ну, эта особенность с "на выходе новое", она в общем-то нужна в языках с одним присваиванием, в остальных можно передать монадическое значение по ссылке и поменять его.
Всё остальное относится к конкретным монадам и рассматривать их нужно отдельно.
Собственно, весь смысл в двух вещах:
- новый тип даёт инкапсуляцию
- явная передача функции-действия развязывает (decoupling) их от собственно процесса применения действия. И основная фишка в том, что этот, своего рода, late binding может происходить не в рантайме (как в технологии COM, если знаете), а во время компиляции. С хорошей поддержкой полиморфизма в системе типов можно:
- гибко определять как будут выполняться одинаковые действия в разных монадах
- повторно использовать однажды определённые действия в новых монадах.