Многие новички в функциональном программировании бывают часто озадачены понятием монады в Haskell’е. Но монады очень часто встречаются в языке, так, например, система ввода/вывода основана именно на понятии монады, а стандартные библиотеки содержат целые модули, посвященные монадам. Необходимо отметить, что понятие монады в Haskell’е основано на теории категорий, однако для того, чтобы не вдаваться в абстрактную математику, далее будет представлено интуитивное понимание монад.
Монады являются типами, которые представляют собой экземпляры одного из следующих монадических классов: Functor, Monad и MonadPlus. Ни один из этих классов не может являться предком для другого класса, т.е. монадические классы не наследуемы. В модуле Prelude определены три монады: IO, [] и Maybe, т.е. список также является монадой.
Математически монада определяется через набор правил, которые связывают операции, производимые над монадой. Эти правила дают интуитивное понимание того, как должна использоваться монада и какова ее внутренняя структура. Для конкретизации далее рассматривается класс Monad, в котором определены две базовые операции и одна функция:
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
return :: a -> m a
fail :: String -> m a
m >> k = m >>= \_ -> k
Две операции (>>=) и (>>) — это операции связывания. Они комбинируют два монадических значения, тогда как функция return преобразует переданное значение некоторого типа a в монадическое значения типа m a. Сигнатура операции (>>) помогает понять операцию связывания: выражение (m a >>= \v ® m b) комбинирует монадическое значение m a, содержащее объект типа a, с функцией, которая оперирует значением v типа a и возвращает результат типа m b. Результатом же комбинации будет монадическое значение типа m b. Операция (>>) используется тогда, когда функция не использует значения, полученного первым монадическим операндом.
Точное значение операция связывания конечно же зависит от конкретной реализации монады. Так, например, тип IO определяет операцию (>>=) как последовательное выполнение двух её операндов, а результат выполнения первого операнда последовательно передается во второй. Для двух других встроенных монадических типов (списки и Maybe) эта операция определена как передача нуля или более параметров из одного вычислительного процесса в следующий.
В Haskell’е определено специальное служебное слово, которое на уровне языка поддерживает использование монад. Это слово do, понимание которого можно увидеть в следующих правилах его применения:
do e1 ; e2 = e1 >> e2
do p <- e1 ; e2 = e1 >>= \p -> e2
Первое выражение выполняется всегда (переноса значений из первого операнда во второй не производится). Во втором выражении может произойти ошибка, в этом случае производится вызов функции fail, которая также определена в классе Monad. Поэтому более точное определение служебного слова do для второго случая будет выглядеть так:
do p <- e1 ; e2 = e1 >>= (\v -> case v of
p -> e2
_ -> fail ”s”)
где s — это строка, которая может определять местоположение оператора do в программе, а может просто нести некоторую семантическую нагрузку. Например, в монаде IO действие (‘a’ ¬ getChar) вызовет функцию fail в том случае, если считанный символ не является символом ‘a’. Это действие прерывает исполнение программы, т.к. определенная в монаде IO функция fail в свою очередь вызывает системную функцию error.
Класс MonadPlus используется для тех монад, в которых есть нулевой элемент и операция "+". Определение этого класса выглядит следующим образом:
class (Monad m) => MonadPlus m where
mzero :: m a
mplus :: m a -> m a -> m a
Нулевой элемент в этом классе монад подчиняется следующим правилам:
m >>= \x -> mzero = mzero
mzero >>= m = mzero
Например, для списков нулевым элементом является пустой список [], а операцией "+" — конкатенация списков. Поэтому монада списков является экземпляром класса MonadPlus. С другой стороны монада IO не имеет нулевого элемента, поэтому монада IO является экземпляром только класса Monad.