Класс MonadPlus и функция guard

Генераторы списков позволяют нам фильтровать наши выходные данные. Например, мы можем отфильтровать список чисел в поиске только тех из них, которые содержат цифру 7:

ghci> [x | x <– [1..50], '7' `elem` show x]

[7,17,27,37,47]

Мы применяем функцию show к параметру x чтобы превратить наше число в строку, а затем проверяем, является ли символ '7' частью этой строки.

Чтобы увидеть, как фильтрация в генераторах списков преобразуется в списковую монаду, мы должны рассмотреть функцию guard и класс типов MonadPlus.

Класс типов MonadPlus предназначен для монад, которые также могут вести себя как моноиды. Вот его определение:

class Monad m => MonadPlus m where

   mzero :: m a

   mplus :: m a –> m a –> m a

Функция mzero является синонимом функции mempty из класса типов Monoid, а функция mplus соответствует функции mappend. Поскольку списки являются моноидами, а также монадами, их можно сделать экземпляром этого класса типов:

instance MonadPlus [] where

   mzero = []

   mplus = (++)

Для списков функция mzero представляет недетерминированное вычисление, которое вообще не имеет результата – неуспешно окончившееся вычисление. Функция mplus сводит два недетерминированных значения в одно. Функция guard определена следующим образом:

guard :: (MonadPlus m) => Bool –> m ()

guard True = return ()

guard False = mzero

Функция guard принимает значение типа Bool. Если это значение равно True, функция guard берёт пустой кортеж () и помещает его в минимальный контекст, который по-прежнему является успешным. Если значение типа Bool равно False, функция guard создаёт монадическое значение с неудачей в вычислениях. Вот эта функция в действии:

ghci> guard (5 > 2) :: Maybe ()

Just ()

ghci> guard (1 > 2) :: Maybe ()

Nothing

ghci> guard (5 > 2) :: [()]

[()]

ghci> guard (1 > 2) :: [()]

[]

Выглядит интересно, но чем это может быть полезно? В списковой монаде мы используем её для фильтрации недетерминированных вычислений:

ghci> [1..50] >>= (x –> guard ('7' `elem` show x) >> return x)

[7,17,27,37,47]

Результат аналогичен тому, что был возвращён нашим предыдущим генератором списка. Как функция guard достигла этого? Давайте сначала посмотрим, как она функционирует совместно с операцией >>:

ghci> guard (5 > 2) >> return "клёво" :: [String]

["клёво"]

ghci> guard (1 > 2) >> return "клёво" :: [String]

[]

Если функция guard срабатывает успешно, результатом, находящимся в ней, будет пустой кортеж. Поэтому дальше мы используем операцию >>, чтобы игнорировать этот пустой кортеж и предоставить что-нибудь другое в качестве результата. Однако если функция guard не срабатывает успешно, функция return впоследствии тоже не сработает успешно, потому что передача пустого списка функции с помощью операции >>= всегда даёт в результате пустой список. Функция guard просто говорит: «Если это значение типа Bool равно False, верни неуспешное окончание вычислений прямо здесь. В противном случае создай успешное значение, которое содержит в себе значение-пустышку ()». Всё, что она делает, – позволяет вычислению продолжиться.

Вот предыдущий пример, переписанный в нотации do:

sevensOnly :: [Int]

sevensOnly = do

   x <– [1..50]

   guard ('7' `elem` show x)

   return x

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

ghci> [x | x <– [1..50], '7' `elem` show x]

[7,17,27,37,47]

Поэтому фильтрация в генераторах списков – это то же самое, что использование функции guard.