Supongamos que una función tiene efectos secundarios. Si tomamos todos los efectos que produce como parámetros de entrada y salida, entonces la función es pura para el mundo exterior.
Entonces, para una función impura
f' :: Int -> Int
agregamos el mundo real a la consideración
f :: Int -> RealWorld -> (Int, RealWorld)
-- input some states of the whole world,
-- modify the whole world because of the side effects,
-- then return the new world.
entonces f
es puro de nuevo. Definimos un tipo de datos parametrizados type IO a = RealWorld -> (a, RealWorld)
, por lo que no necesitamos escribir RealWorld tantas veces, y solo podemos escribir
f :: Int -> IO Int
Para el programador, manejar un RealWorld directamente es demasiado peligroso, en particular, si un programador obtiene un valor de tipo RealWorld, podría intentar copiarlo , lo que es básicamente imposible. (Piense en tratar de copiar todo el sistema de archivos, por ejemplo. ¿Dónde lo colocaría?) Por lo tanto, nuestra definición de IO también encapsula los estados de todo el mundo.
Composición de funciones "impuras"
Estas funciones impuras son inútiles si no podemos encadenarlas. Considerar
getLine :: IO String ~ RealWorld -> (String, RealWorld)
getContents :: String -> IO String ~ String -> RealWorld -> (String, RealWorld)
putStrLn :: String -> IO () ~ String -> RealWorld -> ((), RealWorld)
Queremos
- obtener un nombre de archivo de la consola,
- lee ese archivo y
- imprime el contenido de ese archivo en la consola.
¿Cómo lo haríamos si pudiéramos acceder a los estados del mundo real?
printFile :: RealWorld -> ((), RealWorld)
printFile world0 = let (filename, world1) = getLine world0
(contents, world2) = (getContents filename) world1
in (putStrLn contents) world2 -- results in ((), world3)
Vemos un patrón aquí. Las funciones se llaman así:
...
(<result-of-f>, worldY) = f worldX
(<result-of-g>, worldZ) = g <result-of-f> worldY
...
Entonces podríamos definir un operador ~~~
para vincularlos:
(~~~) :: (IO b) -> (b -> IO c) -> IO c
(~~~) :: (RealWorld -> (b, RealWorld))
-> (b -> RealWorld -> (c, RealWorld))
-> (RealWorld -> (c, RealWorld))
(f ~~~ g) worldX = let (resF, worldY) = f worldX
in g resF worldY
entonces podríamos simplemente escribir
printFile = getLine ~~~ getContents ~~~ putStrLn
sin tocar el mundo real
"Impurificación"
Ahora supongamos que también queremos que el contenido del archivo esté en mayúscula. Las mayúsculas son una función pura
upperCase :: String -> String
Pero para llegar al mundo real, tiene que devolver un IO String
. Es fácil levantar tal función:
impureUpperCase :: String -> RealWorld -> (String, RealWorld)
impureUpperCase str world = (upperCase str, world)
Esto se puede generalizar:
impurify :: a -> IO a
impurify :: a -> RealWorld -> (a, RealWorld)
impurify a world = (a, world)
para que impureUpperCase = impurify . upperCase
podamos escribir
printUpperCaseFile =
getLine ~~~ getContents ~~~ (impurify . upperCase) ~~~ putStrLn
(Nota: normalmente escribimos getLine ~~~ getContents ~~~ (putStrLn . upperCase)
)
Estuvimos trabajando con mónadas todo el tiempo.
Ahora veamos lo que hemos hecho:
- Definimos un operador
(~~~) :: IO b -> (b -> IO c) -> IO c
que encadena dos funciones impuras juntas
- Definimos una función
impurify :: a -> IO a
que convierte un valor puro en impuro.
Ahora hacemos la identificación (>>=) = (~~~)
y return = impurify
, ¿y vemos? Tenemos una mónada.
Nota tecnica
Para asegurarse de que sea realmente una mónada, todavía hay algunos axiomas que también deben verificarse:
return a >>= f = f a
impurify a = (\world -> (a, world))
(impurify a ~~~ f) worldX = let (resF, worldY) = (\world -> (a, world )) worldX
in f resF worldY
= let (resF, worldY) = (a, worldX)
in f resF worldY
= f a worldX
f >>= return = f
(f ~~~ impurify) worldX = let (resF, worldY) = f worldX
in impurify resF worldY
= let (resF, worldY) = f worldX
in (resF, worldY)
= f worldX
f >>= (\x -> g x >>= h) = (f >>= g) >>= h
A la izquierda como ejercicio.