Suponha que uma função tenha efeitos colaterais. Se considerarmos todos os efeitos que ele produz como parâmetros de entrada e saída, a função é pura para o mundo exterior.
Então, para uma função impura
f' :: Int -> Int
nós adicionamos o RealWorld à consideração
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.
então f
é puro novamente. Definimos um tipo de dados parametrizado type IO a = RealWorld -> (a, RealWorld)
, portanto, não precisamos digitar o RealWorld tantas vezes e podemos escrever
f :: Int -> IO Int
Para o programador, lidar com um RealWorld diretamente é muito perigoso - em particular, se um programador colocar um valor do tipo RealWorld em suas mãos, ele poderá tentar copiá- lo, o que é basicamente impossível. (Pense em tentar copiar todo o sistema de arquivos, por exemplo. Onde você o colocaria?) Portanto, nossa definição de IO também engloba os estados do mundo inteiro.
Composição das funções "impuras"
Essas funções impuras são inúteis se não pudermos colocá-las juntas. Considerar
getLine :: IO String ~ RealWorld -> (String, RealWorld)
getContents :: String -> IO String ~ String -> RealWorld -> (String, RealWorld)
putStrLn :: String -> IO () ~ String -> RealWorld -> ((), RealWorld)
Nos queremos
- obtenha um nome de arquivo do console,
- leia esse arquivo e
- imprima o conteúdo desse arquivo no console.
Como faríamos se pudéssemos acessar os estados do 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 um padrão aqui. As funções são chamadas assim:
...
(<result-of-f>, worldY) = f worldX
(<result-of-g>, worldZ) = g <result-of-f> worldY
...
Assim, poderíamos definir um operador ~~~
para vinculá-los:
(~~~) :: (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
então poderíamos simplesmente escrever
printFile = getLine ~~~ getContents ~~~ putStrLn
sem tocar o mundo real.
"Impurificação"
Agora, suponha que também desejemos tornar o conteúdo do arquivo em maiúsculas. Maiúsculas é uma função pura
upperCase :: String -> String
Mas para entrar no mundo real, é preciso retornar um IO String
. É fácil levantar essa função:
impureUpperCase :: String -> RealWorld -> (String, RealWorld)
impureUpperCase str world = (upperCase str, world)
Isso pode ser generalizado:
impurify :: a -> IO a
impurify :: a -> RealWorld -> (a, RealWorld)
impurify a world = (a, world)
para que impureUpperCase = impurify . upperCase
possamos escrever
printUpperCaseFile =
getLine ~~~ getContents ~~~ (impurify . upperCase) ~~~ putStrLn
(Nota: Normalmente nós escrevemos getLine ~~~ getContents ~~~ (putStrLn . upperCase)
)
Estávamos trabalhando com mônadas o tempo todo
Agora vamos ver o que fizemos:
- Definimos um operador
(~~~) :: IO b -> (b -> IO c) -> IO c
que une duas funções impuras
- Definimos uma função
impurify :: a -> IO a
que converte um valor puro em impuro.
Agora fazemos a identificação (>>=) = (~~~)
e return = impurify
, e vê? Temos uma mônada.
Nota técnica
Para garantir que seja realmente uma mônada, ainda existem alguns axiomas que precisam ser verificados também:
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
Esquerda como exercício.