2016-11-07 9 views
2

Являясь функциональным разработчиком Javascript с неопределенным пониманием Haskell, мне действительно трудно понять иконы Haskell, такие как монады. Когда я смотрю на >>= функции, напримерПочему привязка экземпляра функции подает исходное значение на следующее вычисление?

(>>=) :: (r -> a) -> (a -> (r -> b)) -> r -> b 

instance Monad ((->) r) where 
f >>= k = \ r -> k (f r) r 

// Javascript: 

и его применение с Javascript

const bind = f => g => x => g(f(x)) (x); 

const inc = x => x + 1; 

const f = bind(inc) (x => x <= 5 ? x => x * 2 : x => x * 3); 


f(2); // 4 
f(5); // 15 

монадическая функция (a -> (r -> b)) (или (a -> m b)) обеспечивает способ выбора следующего вычисления в зависимости от предыдущего результата. В более общем плане, монадическая функция вместе с ее соответствующим оператором bind, по-видимому, дает нам возможность определить, что представляет собой композиция функций в конкретном вычислительном контексте.

Тем более удивительно, что монадическая функция не дает результат предыдущего вычисления следующему. Вместо этого передается исходное значение. Я ожидал бы f(2)/f(5), чтобы получить 6/18, аналогично нормальной функциональной композиции. Является ли это поведение специфичным для функций как монады? Что я неправильно понимаю?

+0

Правило большого пальца: 'bind (...) (x => ...)' принимает значение предыдущего вычисления и связывает его с 'x'. Другие lambdas 'x => ...' (а не после привязки) вместо этого получают доступ к одному и тому же неявному аргументу только для чтения, который сначала передается в 'f'. – chi

+3

Сторона примечания: 'x => x <= 5 ? x => x * 2: x => x * 3' нечитабельно, пожалуйста, используйте, например. 'x => x <= 5 ? y => y * 2: z => z * 3' - компьютер не заботится об альфа-эквивалентности, но люди делают ;-) – chi

+0

Потому что он * должен * - эта функция является единственной действительной реализацией функция, тип которой 'forall rab. (r -> a) -> (a -> (r -> b)) -> r -> b' (кроме неопределенного, конечно). Функция javascript может, конечно, делать то, что ему нравится, в отсутствие формальных типов, но тогда рассуждение об этом было бы намного сложнее (и назвал его «bind» было бы просто ошибкой). – user2407038

ответ

5

Я думаю, что ваше путаница возникает из-за слишком простых функций. В частности, вы пишете

const inc = x => x + 1; 

тип которого является функцией, которая возвращает значения в же пространство как его вход. Скажем, inc имеет дело с целыми числами. Поскольку и его вход и выход являются целыми числами, если у вас есть другая функция foo, которая принимает целые числа, легко представить, используя выход из inc как вход - foo.

Реальный мир включает в себя более интересные функции. Рассмотрим функцию tree_of_depth, которая берет целое число и создает дерево строк этой глубины. (Я не буду пытаться реализовать его, потому что я не знаю достаточно javascript, чтобы сделать убедительную работу.) Теперь неожиданно трудно представить, как передать вывод tree_of_depth в качестве входа в foo, поскольку foo является ожидая целых чисел, и tree_of_depth производит деревья, не так ли? Единственное, что мы можем передать foo, - это вход - tree_of_depth, потому что это единственное целое число, которое мы имеем, даже после запуска tree_of_depth.

Давайте посмотрим, как это проявляется в типе подписи Haskell для привязки:

(>>=) :: (r -> a) -> (a -> r -> b) -> (r -> b) 

Это говорит о том, что (>>=) принимает два аргумента, каждая из функций. Первая функция может быть любого старого типа, который вам нравится - он может принимать значение типа r и производить значение типа a. В частности, вам не обязательно обещать, что r и a такие же.Но как только вы выбираете его тип, тогда тип следующего аргумента функции для (>>=) ограничен: он должен быть функцией двух аргументов, типы которых равны тем же и a, как и раньше.

Теперь вы можете понять, почему мы должны передать такое же значение типа r на обе эти функции: первая функция производит a, не обновленный r, поэтому мы не имеем никакого другого значения типа r перейти к вторая функция! В отличие от вашей ситуации с inc, где первая функция произошла также с , произведитеr, мы можем производить некоторые другие очень разные типы.

Это объясняет, почему привязка должна быть реализована так, как она есть, но, возможно, не объясняет, почему эта монада является полезной. Там написано в другом месте. Но канонический вариант использования - для переменных конфигурации. Предположим, что при запуске программы вы разбираете файл конфигурации; то для остальной части программы вы хотите иметь возможность влиять на поведение различных функций, просматривая информацию из этой конфигурации. Во всех случаях имеет смысл использовать одну и ту же конфигурационную информацию - ее не нужно менять. Затем эта монада становится полезной: вы можете иметь неявное значение конфигурации, а операция привязки монады гарантирует, что обе функции, которые вы упорядочиваете, имеют доступ к этой информации, не передавая ее вручную обеим функциям.

P.S. Вы говорите

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

который я нахожу немного неточным: ведь в m >>= f, функция f получает как результатом m (в качестве первого аргумента) и исходное значение (в качестве второго аргумента).

+0

Отличный, обширный ответ, спасибо! 'r' и' a' могут быть разных типов. И с заданным типом для '>> =', никакая другая действительная реализация невозможна. Что мне нравится в '(->)' экземплярах Monads, Applicatives и Functors, так это то, что они являются знакомой концепцией (комбинаторами), применяемой в рамках другой концепции, что для меня довольно странно. Они являются подходящей отправной точкой для понимания этой новой концепции. Программирование на математические интерфейсы сложно, особенно в Javascript, где вы можете просто набраться сил. Наверное, я сделал еще один шаг вперед. Еще раз спасибо! – ftor

+0

Функции можно рассматривать как своего рода конструктор типов, поскольку они могут преобразовывать 'r' в' a' другого типа. Когда 'ma' /' mb' внутри '(>> =) :: ma -> (a -> mb) -> mb' являются функциями типа' (r -> a) '/' (r -> b) ', нет альтернативы для' (r -> b) 'игнорировать результат' a' предыдущей операции. При условии, что '(r -> a)' имеет тип '(r -> r)', вы можете использовать комбинатор 'K' для передачи вычисленного значения в' (r -> b) ', потому что' K' является ' return' для экземпляра функции. Извините, если я повторюсь, но, наконец, понял. – ftor

2

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

Я не уверен, что вы подразумеваете под «монадической функцией». Монады (которые в Haskell состоят из функции связывания и чистой функции) позволяют выразить , как последовательность монадических действий может быть скована вместе. ((<=<) - это монадный эквивалент композиции, эквивалентный (.) для монады Identity). В этом смысле вы получаете какую-то композицию, но только состав действий (функции формы a -> m b).

(Это дополнительно отведенный в Kleisli Newtype вокруг функций типа a -> m b. Его category instance действительно позволяет писать секвенирование монадических действий как состав.)

Я бы ожидать, е (2)/f (5), чтобы получить 6/18, аналогично нормальному функциональному составу.

Затем вы можете просто использовать нормальную композицию! Не используйте монаду, если она вам не нужна.

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

Да, это так. Монада Monad ((->) r) также известна как «монада-читателя», потому что только читает из своей среды. Тем не менее, что касается монад, вы все еще передаете монадический результат предыдущего действия на последующий, но эти результаты сами являются функциями!

+0

ОК, я больше не буду использовать монадическую функцию, но _action_ или _kleisli arrow_. Благодаря! – ftor

2

Как уже упомянутые чи, эта линия

const f = bind(inc) (x => x <= 5 ? x => x * 2 : x => x * 3); 

будет понятнее, как что-то вроде

const f = bind(inc) (x => x <= 5 ? y => y * 2 : y => y * 3); 

Monad экземпляр для функций в основном Reader монады. У вас есть значение x => x + 1, которое зависит от окружающей среды (добавляет 1 к окружающей среде).

У вас также есть функция, которая в зависимости от ее ввода возвращает одно значение, которое зависит от среды (y => y * 2) или другое значение, зависящее от среды (y => y * 3).

В вашем bind вы используете только результат x => x + 1 по номеру . Выберите между этими двумя функциями. Вы не возвращаете предыдущий результат напрямую. Но вы могли бы, если вы вернулись постоянные функции которые игнорировали их среду и возвращаемой фиксированное значение, зависящее только от предыдущего результата:

const f = bind(inc) (x => x <= 5 ? _ => x * 2 : _ => x * 3); 

(не уверен в синтаксисе)

+0

Спасибо за ваш ответ. Наверное, ваш последний пример - это довольно необычное использование 'bind', правильно? – ftor

+0

Совсем нет! Каждое определение экземпляра 'Monad' требует, помимо' bind', другой функции, которая ставит чистое значение в «нейтральном» контексте. В Haskell другая функция (смущающе) называется 'return'. 'return' для монодатчиков функции/Reader только в постоянной функции, которая получает среду и игнорирует ее. – danidiaz

+1

ah, 'return' экземпляра' (->) 'является комбинатором' K'. Понял! – ftor