Benjamin Pierce dijo en TAPL
Se puede considerar que un sistema de tipos calcula un tipo de aproximación estática a los comportamientos de tiempo de ejecución de los términos en un programa.
Es por eso que un lenguaje equipado con un poderoso sistema de tipos es estrictamente más expresivo que un lenguaje mal escrito. Puedes pensar en las mónadas de la misma manera.
Como @Carl y sigfpe point, puede equipar un tipo de datos con todas las operaciones que desee sin recurrir a mónadas, clases de tipos o cualquier otra cosa abstracta. Sin embargo, las mónadas le permiten no solo escribir código reutilizable, sino también abstraer todos los detalles redundantes.
Como ejemplo, digamos que queremos filtrar una lista. La forma más simple es usar elfilter
función:, filter (> 3) [1..10]
que es igual [4,5,6,7,8,9,10]
.
Una versión un poco más complicada de filter
, que también pasa un acumulador de izquierda a derecha, es
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]
Para obtener todo i
, de modo que i <= 10, sum [1..i] > 4, sum [1..i] < 25
podamos escribir
filterAccum (\a x -> let a' = a + x in (a' > 4 && a' < 25, a')) 0 [1..10]
que es igual [3,4,5,6]
.
O podemos redefinir la nub
función, que elimina elementos duplicados de una lista, en términos de filterAccum
:
nub' = filterAccum (\a x -> (x `notElem` a, x:a)) []
nub' [1,2,4,5,4,3,1,8,9,4]
es igual [1,2,4,5,3,8,9]
. Aquí se pasa una lista como acumulador. El código funciona, porque es posible salir de la mónada de la lista, por lo que todo el cómputo se mantiene puro (en realidad notElem
no se usa >>=
, pero podría). Sin embargo, no es posible abandonar con seguridad la mónada IO (es decir, no puede ejecutar una acción IO y devolver un valor puro; el valor siempre estará envuelto en la mónada IO). Otro ejemplo son las matrices mutables: después de haber dejado la mónada ST, donde vive una matriz mutable, ya no puede actualizar la matriz en tiempo constante. Entonces necesitamos un filtro monádico del Control.Monad
módulo:
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
ejecuta una acción monádica para todos los elementos de una lista, produciendo elementos, para los cuales la acción monádica regresa True
.
Un ejemplo de filtrado con una matriz:
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]
imprime [1,2,4,5,3,8,9]
como se esperaba.
Y una versión con la mónada IO, que pregunta qué elementos devolver:
main = filterM p [1,2,4,5] >>= print where
p i = putStrLn ("return " ++ show i ++ "?") *> readLn
P.ej
return 1? -- output
True -- input
return 2?
False
return 4?
False
return 5?
True
[1,5] -- output
Y como ilustración final, filterAccum
se puede definir en términos de filterM
:
filterAccum f a xs = evalState (filterM (state . flip f) xs) a
con el StateT
mónada, que se usa debajo del capó, siendo solo un tipo de datos ordinario.
Este ejemplo ilustra que las mónadas no solo le permiten abstraer el contexto computacional y escribir código reutilizable limpio (debido a la componibilidad de las mónadas, como explica @Carl), sino también tratar los tipos de datos definidos por el usuario y las primitivas integradas de manera uniforme.