Se as linguagens de programação funcional não podem salvar nenhum estado, como eles fazem algumas coisas simples, como ler a entrada de um usuário (quero dizer, como eles "armazenam"), ou armazenar quaisquer dados para esse assunto?
Como você percebeu, a programação funcional não tem estado - mas isso não significa que não pode armazenar dados. A diferença é que se eu escrever uma declaração (Haskell) ao longo das linhas de
let x = func value 3.14 20 "random"
in ...
Tenho a garantia de que o valor de x
é sempre o mesmo em ...
: nada pode alterá-lo. Da mesma forma, se eu tiver uma função f :: String -> Integer
(uma função que recebe uma string e retorna um inteiro), posso ter certeza de que f
não modificarei seu argumento, nem alterarei nenhuma variável global, nem gravarei dados em um arquivo e assim por diante. Como sepp2k disse em um comentário acima, esta não mutabilidade é realmente útil para raciocinar sobre programas: você escreve funções que dobram, giram e mutilam seus dados, retornando novas cópias para que você possa encadea-los, e você pode ter certeza de que nenhum dessas chamadas de função pode fazer qualquer coisa "prejudicial". Você sabe que x
é sempre x
, e não precisa se preocupar se alguém escreveu x := foo bar
em algum lugar entre a declaração dex
e seu uso, porque isso é impossível.
Agora, e se eu quiser ler a entrada de um usuário? Como KennyTM disse, a ideia é que uma função impura é uma função pura que é passada para o mundo inteiro como um argumento e retorna seu resultado e o mundo. Claro, você não quer realmente fazer isso: por um lado, é terrivelmente desajeitado e, por outro, o que acontece se eu reutilizar o mesmo objeto de mundo? Portanto, isso é abstraído de alguma forma. Haskell lida com isso com o tipo IO:
main :: IO ()
main = do str <- getLine
let no = fst . head $ reads str :: Integer
...
Isso nos diz que main
é uma ação IO que não retorna nada; executar esta ação é o que significa executar um programa Haskell. A regra é que os tipos IO nunca podem escapar de uma ação IO; neste contexto, apresentamos essa ação usando do
. Assim, getLine
retorna um IO String
, que pode ser pensado de duas maneiras: primeiro, como uma ação que, quando executada, produz uma string; em segundo lugar, como uma string "manchada" por IO, pois foi obtida de maneira impura. O primeiro é mais correto, mas o segundo pode ser mais útil. O <-
tira o String
do IO String
e o armazena - str
mas como estamos em uma ação de IO, teremos que embrulhá-lo novamente, para que não possa "escapar". A próxima linha tenta ler um inteiro ( reads
) e pega a primeira correspondência bem-sucedida (fst . head
); tudo isso é puro (sem IO), então damos um nome a ele com let no = ...
. Podemos então usar no
e str
no ...
. Assim, armazenamos dados impuros (de getLine
em str
) e dados puros ( let no = ...
).
Este mecanismo para trabalhar com IO é muito poderoso: ele permite separar a parte pura e algorítmica de seu programa da parte impura de interação com o usuário, e reforça isso no nível de tipo. Sua minimumSpanningTree
função não pode mudar algo em algum outro lugar em seu código ou escrever uma mensagem para seu usuário e assim por diante. É seguro.
Isso é tudo que você precisa saber para usar IO em Haskell; se isso é tudo que você quer, pode parar por aqui. Mas se você quiser entender por que isso funciona, continue lendo. (E observe que essas coisas serão específicas para Haskell - outras linguagens podem escolher uma implementação diferente.)
Portanto, isso provavelmente parecia uma trapaça, de alguma forma adicionando impureza ao puro Haskell. Mas não é - acontece que podemos implementar o tipo IO inteiramente dentro de Haskell puro (contanto que tenhamos RealWorld
). A ideia é esta: uma ação IO IO type
é o mesmo que uma função RealWorld -> (type, RealWorld)
, que pega o mundo real e retorna um objeto do tipo type
e o modificado RealWorld
. Em seguida, definimos algumas funções para que possamos usar este tipo sem enlouquecer:
return :: a -> IO a
return a = \rw -> (a,rw)
(>>=) :: IO a -> (a -> IO b) -> IO b
ioa >>= fn = \rw -> let (a,rw') = ioa rw in fn a rw'
O primeiro nos permite falar sobre ações IO que não fazem nada: return 3
é uma ação IO que não consulta o mundo real e apenas retorna 3
. O >>=
operador, pronunciado "vincular", nos permite executar ações IO. Ele extrai o valor da ação IO, passa-o e ao mundo real por meio da função e retorna a ação IO resultante. Observe que isso >>=
impõe nossa regra de que os resultados das ações de IO nunca podem escapar.
Podemos então transformar o anterior main
no seguinte conjunto comum de aplicativos de função:
main = getLine >>= \str -> let no = (fst . head $ reads str :: Integer) in ...
O tempo de execução de Haskell começa main
com o inicial RealWorld
e está pronto! Tudo é puro, tem apenas uma sintaxe sofisticada.
[ Editar: como @Conal aponta , isso não é realmente o que Haskell usa para fazer IO. Este modelo quebra se você adicionar simultaneidade, ou mesmo qualquer forma de o mundo mudar no meio de uma ação de IO, portanto, seria impossível para Haskell usar este modelo. É preciso apenas para computação sequencial. Portanto, pode ser que o IO de Haskell seja um pouco um esquivo; mesmo se não for, certamente não é tão elegante. De acordo com a observação de @ Conal, veja o que Simon Peyton-Jones diz em Tackling the Awkward Squad [pdf] , seção 3.1; ele apresenta o que pode equivaler a um modelo alternativo ao longo dessas linhas, mas depois o descarta por sua complexidade e adota uma abordagem diferente.]
Novamente, isso explica (praticamente) como o IO e a mutabilidade em geral funcionam em Haskell; se isso é tudo que você quer saber, pode parar de ler aqui. Se você quiser uma última dose de teoria, continue lendo - mas lembre-se, neste ponto, já fomos muito longe de sua pergunta!
Então, uma última coisa: acontece que essa estrutura - um tipo paramétrico com return
e >>=
- é muito geral; é chamado de mônada, e a do
notação return
, e >>=
funciona com qualquer um deles. Como você viu aqui, as mônadas não são mágicas; tudo o que é mágico é que os do
blocos se transformam em chamadas de função. O RealWorld
tipo é o único lugar onde vemos alguma magia. Tipos como []
, o construtor de lista, também são mônadas e não têm nada a ver com código impuro.
Você agora sabe (quase) tudo sobre o conceito de mônada (exceto algumas leis que devem ser satisfeitas e a definição matemática formal), mas falta-lhe intuição. Há um número ridículo de tutoriais de mônadas online; Eu gosto deste , mas você tem opções. No entanto, isso provavelmente não o ajudará ; a única maneira real de obter a intuição é combinando usá-los com a leitura de alguns tutoriais no momento certo.
No entanto, você não precisa dessa intuição para entender IO . Compreender as mônadas em geral é a cereja do bolo, mas você pode usar IO agora. Você poderia usá-lo depois que eu mostrasse a primeira main
função. Você pode até tratar o código IO como se estivesse em uma linguagem impura! Mas lembre-se de que há uma representação funcional subjacente: ninguém está trapaceando.
(PS: Desculpe pela extensão. Fui um pouco mais longe.)