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 fnã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 barem 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, getLineretorna 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 Stringdo IO Stringe o armazena - strmas 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 noe strno .... Assim, armazenamos dados impuros (de getLineem 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 minimumSpanningTreefunçã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 typee 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 mainno 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 maincom o inicial RealWorlde 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 returne >>=- é muito geral; é chamado de mônada, e a donotaçã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 doblocos se transformam em chamadas de função. O RealWorldtipo é 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 mainfunçã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.)