Представления № 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
к каждому из них и собрать все результаты в один список. Выражается в Хаскеле
return :: a -> [a]
return x = [x]
(<=<) :: (b -> [c]) -> (a -> [b]) -> (a -> [c])
g (<=<) f = concat . map g . f
или используя >>=
(>>=) :: [a] -> (a -> [b]) -> [b]
x >>= f = concat (map f x)
Обратите внимание, что в этом примере возвращаемые типы были [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
.