Бенджамин Пирс сказал в TAPL
Систему типов можно рассматривать как вычисление своего рода статического приближения к поведению терминов в программе во время выполнения.
Вот почему язык, оснащенный мощной системой типов, строго более выразителен, чем плохо типизированный язык. Вы можете думать о монадах таким же образом.
Как @Carl и Sigfpe , вы можете оборудовать тип данных всеми вам операциями, не прибегая к монадам, классам типов или любым другим абстрактным вещам. Однако монады позволяют вам не только писать повторно используемый код, но и абстрагироваться от всех лишних деталей.
В качестве примера, скажем, мы хотим отфильтровать список. Самый простой способ - использовать filter
функцию:, filter (> 3) [1..10]
которая равна[4,5,6,7,8,9,10]
.
Несколько более сложная версия filter
, которая также проходит аккумулятор слева направо,
swap (x, y) = (y, x)
(.*) = (.) . (.)
filterAccum :: (a -> b -> (Bool, a)) -> a -> [b] -> [b]
filterAccum f a xs = [x | (x, True) <- zip xs $ snd $ mapAccumL (swap .* f) a xs]
Чтобы получить все i
, что i <= 10, sum [1..i] > 4, sum [1..i] < 25
мы можем написать
filterAccum (\a x -> let a' = a + x in (a' > 4 && a' < 25, a')) 0 [1..10]
что равно [3,4,5,6]
.
Или мы можем переопределить nub
функцию, которая удаляет повторяющиеся элементы из списка, с точки зрения filterAccum
:
nub' = filterAccum (\a x -> (x `notElem` a, x:a)) []
nub' [1,2,4,5,4,3,1,8,9,4]
равно [1,2,4,5,3,8,9]
. Список передается здесь как аккумулятор. Код работает, потому что можно оставить монаду списка, поэтому все вычисления остаются чистыми ( notElem
на >>=
самом деле не используются , но могут). Однако невозможно безопасно покинуть монаду ввода-вывода (т.е. вы не можете выполнить действие ввода-вывода и вернуть чистое значение - значение всегда будет заключено в монаду ввода-вывода). Другой пример - изменяемые массивы: после того, как вы оставили монаду ST, в которой находится изменяемый массив, вы больше не можете обновлять массив в постоянное время. Итак, нам нужна монадическая фильтрация из Control.Monad
модуля:
filterM :: (Monad m) => (a -> m Bool) -> [a] -> m [a]
filterM _ [] = return []
filterM p (x:xs) = do
flg <- p x
ys <- filterM p xs
return (if flg then x:ys else ys)
filterM
выполняет монадическое действие для всех элементов списка, получая элементы, для которых возвращается монадическое действие True
.
Пример фильтрации с массивом:
nub' xs = runST $ do
arr <- newArray (1, 9) True :: ST s (STUArray s Int Bool)
let p i = readArray arr i <* writeArray arr i False
filterM p xs
main = print $ nub' [1,2,4,5,4,3,1,8,9,4]
печатает [1,2,4,5,3,8,9]
как ожидалось.
И версия с монадой ввода-вывода, которая спрашивает, какие элементы возвращать:
main = filterM p [1,2,4,5] >>= print where
p i = putStrLn ("return " ++ show i ++ "?") *> readLn
Например
return 1? -- output
True -- input
return 2?
False
return 4?
False
return 5?
True
[1,5] -- output
И в качестве окончательной иллюстрации, filterAccum
можно определить с точки зрения filterM
:
filterAccum f a xs = evalState (filterM (state . flip f) xs) a
с StateT
монадой, которая используется под капотом, будучи просто обычным типом данных.
Этот пример иллюстрирует, что монады позволяют не только абстрагировать вычислительный контекст и писать чистый повторно используемый код (благодаря компоновке монад, как объясняет @Carl), но также и обрабатывать пользовательские типы данных и встроенные примитивы единообразно.