Во время изучения Haskell я столкнулся с множеством учебных пособий, в которых пытался объяснить, что такое монады и почему они важны в Haskell. Каждый из них использовал аналогии, чтобы было легче понять смысл. В конце концов, я получил 3 разных взгляда на то, что такое монада:
Вид 1: Монада как ярлык
Иногда мне кажется, что монада как ярлык для конкретного типа. Например, функция типа:
myfunction :: IO Int
myfunction - это функция, которая при каждом выполнении выдает значение Int. Тип результата - не Int, а IO Int. Итак, IO - это метка значения Int, предупреждающая пользователя о том, что значение Int является результатом процесса, в котором было выполнено действие IO.
Следовательно, это значение Int было помечено как значение, полученное из процесса с IO, поэтому это значение является «грязным». Ваш процесс больше не чист.
Вид 2: Монада как личное пространство, где могут происходить неприятные вещи.
В системе, где все процессы чистые и строгие, иногда вам нужны побочные эффекты. Итак, монада - это всего лишь небольшое пространство, которое позволяет вам делать неприятные побочные эффекты. В этом пространстве вам позволено покинуть чистый мир, стать нечистым, совершить свой процесс, а затем вернуться с ценностью.
Вид 3: Монада как в теории категорий
Это мнение, которое я не до конца понимаю. Монада - это просто функтор той же категории или подкатегории. Например, у вас есть значения Int и в качестве подкатегории IO Int, которые являются значениями Int, сгенерированными после процесса ввода-вывода.
Верны ли эти взгляды? Что является более точным?
Ответы:
Представления № 1 и № 2 в целом неверны.
* -> *
может работать как метка, монады - гораздо больше.IO
монады) вычисления внутри монады не являются нечистыми. Они просто представляют вычисления, которые мы воспринимаем как имеющие побочные эффекты, но они чисты.Оба эти недоразумения происходят от сосредоточенности на
IO
монаде, которая на самом деле немного особенная.Я постараюсь немного подробнее остановиться на №3, не вдаваясь в теорию категорий, если это возможно.
Стандартные вычисления
Все вычисления в функциональном языке программирования можно рассматривать как функции с типом источника и целевого типа:
f :: a -> b
. Если функция имеет более одного аргумента, мы можем преобразовать ее в функцию с одним аргументом с помощью карри (см. Также вики Haskell ). И если у нас есть только значениеx :: a
(функция с 0 аргументов), мы можем превратить его в функцию , которая принимает аргумент типа блока :(\_ -> x) :: () -> a
.Мы можем создавать более сложные программы из более простых, составляя такие функции с помощью
.
оператора. Например, если у нас естьf :: a -> b
иg :: b -> c
мы получимg . f :: a -> c
. Обратите внимание, что это работает и для наших преобразованных значений: если у нас естьx :: a
и преобразовать его в наше представление, мы получаемf . ((\_ -> x) :: () -> a) :: () -> b
.Это представление имеет несколько очень важных свойств, а именно:
id :: a -> a
для каждого типаa
. Это элемент идентичности по отношению к.
:f
равенf . id
иid . f
..
является ассоциативным .Монадические вычисления
Предположим, мы хотим выбрать и поработать с какой-то особой категорией вычислений, результат которой содержит нечто большее, чем просто возвращаемое значение. Мы не хотим уточнять, что означает «что-то большее», мы хотим сделать вещи как можно более общими. Самый общий способ представить «нечто большее» - это представить его как функцию типа - тип
m
типа* -> *
(то есть он преобразует один тип в другой). Поэтому для каждой категории вычислений, с которой мы хотим работать, у нас будет некоторая функция типаm :: * -> *
. (В Haskell,m
есть[]
,IO
,Maybe
и т.д.) И категория содержит все функции типовa -> m b
.Теперь нам хотелось бы работать с функциями в такой категории так же, как и в базовом случае. Мы хотим иметь возможность составлять эти функции, мы хотим, чтобы композиция была ассоциативной, и мы хотим иметь идентичность. Нам нужно:
<=<
), который объединяет функцииf :: a -> m b
иg :: b -> m c
превращает их во что-то вродеg <=< f :: a -> m c
. И это должно быть ассоциативно.return
. Мы также хотим, чтобыf <=< return
это было так же, какf
и так же, какreturn <=< f
.Любой,
m :: * -> *
для которого у нас есть такие функцииreturn
и<=<
называется монадой . Это позволяет нам создавать сложные вычисления из более простых, как в базовом случае, но теперь типы возвращаемых значений преобразуютсяm
.(На самом деле, я немного злоупотребил термином категория здесь. В смысле теории категорий мы можем назвать нашу конструкцию категорией только после того, как узнаем, что она подчиняется этим законам.)
Монады в Хаскеле
В Haskell (и других функциональных языках) мы в основном работаем со значениями, а не с функциями типов
() -> a
. Таким образом, вместо определения<=<
для каждой монады, мы определяем функцию(>>=) :: m a -> (a -> m b) -> m b
. Такое альтернативное определение эквивалентно, мы можем выразить,>>=
используя<=<
и наоборот (попробуйте в качестве упражнения, или посмотрите источники ). Принцип менее очевиден сейчас, но он остается тем же: наши результаты всегда имеют типы,m a
и мы составляем функции типовa -> m b
.Для каждой монады, которую мы создаем, мы не должны забывать проверять это
return
и<=<
иметь требуемые нам свойства: ассоциативность и тождество слева / справа. Выражается с помощьюreturn
и>>=
они называются законами монады .Пример - списки
Если мы решим
m
быть[]
, мы получим категорию функций типовa -> [b]
. Такие функции представляют недетерминированные вычисления, результатом которых может быть одно или несколько значений, но также нет значений. Это приводит к так называемой монаде списка . Составf :: a -> [b]
иg :: b -> [c]
работает следующим образом:g <=< f :: a -> [c]
означает вычислить все возможные результаты типа[b]
, применитьg
к каждому из них и собрать все результаты в один список. Выражается в Хаскелеили используя
>>=
Обратите внимание, что в этом примере возвращаемые типы были
[a]
настолько вероятными, что они не содержали никакого значения типаa
. Действительно, для монады не существует такого требования, чтобы у возвращаемого типа были такие значения. У некоторых монад всегда есть (какIO
илиState
), но у некоторых нет, как[]
илиMaybe
.IO монада
Как я уже говорил,
IO
монада - это нечто особенное. Значение типаIO a
означает значение типа,a
созданное путем взаимодействия с программной средой. Таким образом (в отличие от всех других монад), мы не можем описать значение типа,IO a
используя некоторую чистую конструкцию. ВотIO
просто тег или метка, которая отличает вычисления, которые взаимодействуют со средой. Это (единственный случай), когда представления № 1 и № 2 являются правильными.Для
IO
монады:f :: a -> IO b
иg :: b -> IO c
означает: вычисление,f
которое взаимодействует со средой, а затем вычисление,g
которое использует значение и вычисляет результат, взаимодействующий со средой.return
просто добавляетIO
«тег» к значению (мы просто «вычисляем» результат, сохраняя среду нетронутой).Некоторые заметки:
m a
, нет способа, как «убежать» отIO
монады. Смысл в следующем: как только вычисление взаимодействует со средой, вы не можете создать вычисление из него, которое этого не делает.IO
монады. Именно поэтомуIO
его часто называют грехом программиста .getChar
должны иметь тип результатаIO something
.источник
IO
не имеет специальной семантики с точки зрения языка. Он не особенный, он ведет себя как любой другой код. Только реализация библиотеки времени выполнения является особенной. Также существует специальный способ escape (unsafePerformIO
). Я думаю, что это важно, потому что люди часто думаютIO
как специальный элемент языка или декларативный тег. Нет.coerce :: a -> b
которая преобразует любые два типа (и в большинстве случаев вылетает из программы). Посмотрите этот пример - вы можете преобразовать даже функцию вInt
и т. Д.runST :: (forall s. GHC.ST.ST s a) -> a
Вид 1: Монада как ярлык
«Следовательно, это значение Int было помечено как значение, полученное из процесса с IO, поэтому это значение« грязное »».
«IO Int», как правило, не является значением Int (хотя в некоторых случаях это может быть, например, «return 3»). Это процедура, которая выводит некоторое значение Int. Разные исполнения этой «процедуры» могут давать разные значения Int.
Монада m является встроенным (обязательным) «языком программирования»: в этом языке можно определить некоторые «процедуры». Монадическое значение (типа ma) - это процедура на этом «языке программирования», которая выводит значение типа a.
Например:
некоторая процедура, которая выводит значение типа Int.
Затем:
это некоторая процедура, которая выводит два (возможно, разных) Ints.
Каждый такой «язык» поддерживает некоторые операции:
две процедуры (ma и mb) могут быть «сцеплены»: вы можете создать более крупную процедуру (ma >> mb), сделанную из первой, а не второй;
более того, выход (a) первого может повлиять на второй (ma >> = \ a -> ...);
процедура (возврат x) может дать некоторое постоянное значение (x).
Различные встроенные языки программирования различаются в зависимости от того, что они поддерживают:
источник
Не путайте монадический тип с классом монад.
Монадический тип (т. Е. Тип, являющийся экземпляром класса монад) решит конкретную проблему (в принципе, каждый монадический тип решает свою задачу): State, Random, Maybe, IO. Все они являются типами с контекстом (то, что вы называете «меткой», но это не то, что делает их монадой).
Для всех из них существует необходимость в «операциях с выбором цепочки» (одна операция зависит от результата предыдущей). Здесь вступает в игру класс монада: пусть ваш тип (решение данной проблемы) будет экземпляром класса монады, и проблема цепочки решена.
Посмотрите, что решает класс монада?
источник