Как вы можете видеть, функция join для списков – это просто concat. Чтобы разгладить значение монады Writer, результат которого сам является значением монады Writer, нам нужно объединить моноидное значение с помощью функции mappend:
ghci> runWriter $ join (Writer (Writer (1, "aaa"), "bbb"))
(1,"bbbaaa")
Внешнее моноидное значение "bbb" идёт первым, затем к нему конкатенируется строка "aaa". На интуитивном уровне, когда вы хотите проверить результат значения типа Writer, сначала вам нужно записать его моноидное значение в журнал, и только потом вы можете посмотреть, что находится внутри него.
Разглаживание значений монады Either очень похоже на разглаживание значений монады Maybe:
ghci> join (Right (Right 9)) :: Either String Int
Right 9
ghci> join (Right (Left "ошибка")) :: Either String Int
Left "ошибка"
ghci> join (Left "ошибка") :: Either String Int
Left "ошибка"
Если применить функцию join к вычислению с состоянием, результат которого является вычислением с состоянием, то результатом будет вычисление с состоянием, которое сначала выполняет внешнее вычисление с состоянием, а затем результирующее. Взгляните, как это работает:
ghci> runState (join (state $ \s –> (push 10, 1:2:s))) [0,0,0]
((),[10,1,2,0,0,0])
Здесь анонимная функция принимает состояние, помещает 2 и 1 в стек и представляет push 10 как свой результат. Поэтому когда всё это разглаживается с помощью функции join, а затем выполняется, всё это выражение сначала помещает значения 2 и 1 в стек, а затем выполняется выражение push 10, проталкивая число 10 на верхушку.
Реализация для функции join такова:
join :: (Monad m) => m (m a) –> m a
join mm = do
m <– mm
m
Поскольку результат mm является монадическим значением, мы берём этот результат, а затем просто помещаем его на его собственную строку, потому что это и есть монадическое значение. Трюк здесь в том, что когда мы вызываем выражение m <– mm, контекст монады, в которой мы находимся, будет обработан. Вот почему, например, значения типа Maybe дают в результате значения Just, только если и внешнее, и внутреннее значения являются значениями Just. Вот как это выглядело бы, если бы значение mm было заранее установлено в Just (Just 8):
joinedMaybes :: Maybe Int
joinedMaybes = do
m <– Just (Just 8)
m
Наверное, самое интересное в функции join – то, что для любой монады передача монадического значения в функцию с помощью операции >>= представляет собой то же самое, что и просто отображение значения с помощью этой функции, а затем использование функции join для разглаживания результирующего вложенного монадического значения! Другими словами, выражение m >>= f – всегда то же самое, что и join (fmap f m). Если вдуматься, это имеет смысл.
При использовании операции >>= мы постоянно думаем, как передать монадическое значение функции, которая принимает обычное значение, а возвращает монадическое. Если мы просто отобразим монадическое значение с помощью этой функции, то получим монадическое значение внутри монадического значения. Например, скажем, у нас есть Just 9 и функция \x –> Just (x+1). Если с помощью этой функции мы отобразим Just 9, у нас останется Just (Just 10).
То, что выражение m >>= f всегда равно join (fmap f m), очень полезно, если мы создаём свой собственный экземпляр класса Monad для некоего типа. Это связано с тем, что зачастую проще понять, как мы бы разгладили вложенное монадическое значение, чем понять, как реализовать операцию >>=.
Ещё интересно то, что функция join не может быть реализована, всего лишь используя функции, предоставляемые функторами и аппликативными функторами. Это приводит нас к заключению, что монады не просто
Функция filterM
Функция filter – это просто хлеб программирования на языке Haskell (при том что функция map – масло). Она принимает предикат и список, подлежащий фильтрации, а затем возвращает новый список, в котором сохраняются только те элементы, которые удовлетворяют предикату. Её тип таков:
filter :: (a –> Bool) –> [a] –> [a]