Entrada e Saída
Em programação dizemos que uma função tem efeitos colaterais quando lê ou modifica qualquer estado além do que lhe foi passado como parâmetro e suas variáveis locais. Linguagens de funcionais puras não tem efeitos colaterais e é por isso que toda vez que uma função é invocada com os mesmos parâmetros, ela retorna o mesmo resultado, assim como 2+2 = 4, sempre.
O problema de ser uma linguagem funcional pura está em realizar Entrada e Saída (E/S, I/O), ou seja, interagir com o mundo exterior ao programa em execução, por exemplo lendo do teclado, manipulando arquivos, ou mesmo modificando o estado da memória de vídeo para desenhar ou escrever algo na tela, pois isso é um efeito colateral. Como E/S é uma realidade dos programas de computador, as linguagens funcionais devem ou abdicar de sua pureza ou usar de artimanhas para realizá-las.
No caso de Haskell, E/S é encapsulada em Mônadas, um conceito no qual não nos aprofundaremos neste curso.
Ações de E/S
E/S é executada por meio de uma ação E/S, uma função especial que se comunica com o mundo "exterior". As seguintes funções são exemplos de ações que se comunicam com teclado e monitor.
Ação | Descrição |
---|---|
getChar |
Lê um caractere do teclado. |
putChar |
Escreve um caractere na tela. |
getLine |
Lê uma linha do teclado. |
putStr |
Escreve uma string na tela. |
putStrLn |
Escreve uma string na tela e quebra a linha. |
binding
Uma vez que tenha visto estas funções, é natural que queira escrever algo como nome = getLine
, mas isso não terá o efeito que você espera.
Prelude> nome = getLine
Prelude> nome
lalala
"lalala"
Prelude> putStr nome
<interactive>:11:8: error:
• Couldn't match type ‘IO String’ with ‘[Char]’
Expected type: String
Actual type: IO String
• In the first argument of ‘putStr’, namely ‘nome’
In the expression: putStr nome
In an equation for ‘it’: it = putStr nome
O que aconteceu no exemplo acima foi que a ação getLine
foi atribuída a nome
, não uma string.
Para fazer o que você quer, isto é, executar uma ação e atribuir o seu resultado a uma variável, precisamos usar o operador <-
.
Prelude> nome <- getLine
lala
Prelude> print nome
"lala"
Dizemos que o operador <-
faz o binding (atrelamento) do valor à variável.
IO
Se explorarmos o tipo das ações veremos que elas não tem retorno como esperaríamos, por exemplo, getLine
tem tipo getLine :: IO String
.
O IO
no tipo do resultado é um tipo algébrico que encapsula o resultado da ação, mas diferenciar a ação de outras funções que resultem em String
.
Todas as ações retornam um IO a
, onde a
é uma variável de tipo, mesmo aquelas que não retornam nada de útil, como a ação putChar
, que resulta em putChar :: Char -> IO ()
, isto é, o encapsulamento da tupla vazia (também conhecida como Unit).
Mas por quê esta diferenciação? Para marcar funções impuras e separá-las das funções puras. Este estigma de uma função impura serve para que o compilador Haskell determine quando a ação pode executar e quando não pode, sendo que uma função impura só pode executar dentro de outra função impura. Isso quer dizer que eu não consigo fazer uma ação de E/S e retornar somente o resultado desta ação, como em
lêNomePura :: String
lêNomePura = x <- getLine
Na verdade, este código nem compila, pois o que é o resultado da expressão à direita na equação? Na verdade, ela nem é uma expressão.
> :t x <- getLine
<no location info>: error: not an expression: ‘x <- getLine’
Uma outra tentativa seria a seguinte. Mas como vimos, este código não executa a ação, apenas dá outro nome para ação getLine
.
lêNomeImpura :: IO String
lêNomeImpura = getLine
do
O problema aqui é que você não está descrevendo como um cálculo acontece, mas dizendo ao processo que execute ações.
Isso está muito mais para o paradigma imperativo que para o funcional.
E de fato, para executar ações, você precisara usar o modo imperativo do Haskell, usando do
.
lêNomeImpura' :: IO String
lêNomeImpura' = do
putStr "Digite o seu nome"
getLine
O modo imperativo essencialmente diz que as ações devem ser executadas na ordem em que foram definidas e que o resultado do bloco é o resultado da última ação do bloco, neste exemplo, getLine
.
return
Nem sempre o resultado será gerado pela última ação, e neste caso você terá que construir o resultado manualmente, como no código a seguir, usando um return
no final.
lêNomeImpura' :: IO String
lêNomeImpura' = do
putStr "Digite o seu nome"
x <- getLine
putStr "Obrigado"
return ("O nome é : " ++ x)
O return
pode causar confusão, pois ele não faz o que você imagina. O que ele faz é construir um valor do tipo IO
com o valor passado, e que pode ser usado como resposta para o bloco.
> x = return 3
> x
3
> :t x
x :: (Monad m, Num a) => m a
let in
Frequentemente você precisará intercalar código puro com código não puro, como no exemplo incorreto a seguir.
import Data.Char
main = do
putStrLn "Digite seu nome: "
nome <- getLine
nomeMaiúsculo = map toUpper nome
nomeMinúsculo = map toLower nome
putStrLn $ "Olá. Seu nome em letras maiúsculas é " ++ nomeMaiúsculo ++ " e em letras minúsculas é " ++ nomeMinúsculo
Este exemplo, embora se assemelhe ao que outras linguagens fazem, não é correto em Haskell pois o código puro não pode ser invocado de forma imperativa, pois códigos puros são definições de equações, não instruções de execução.
A forma apropriada para fazer as definições de nomeMaiúsculo
e nomeMinúsculo
de forma que sejam usáveis na invocação de putStrLn
é via let
e in
; bom, neste caso, como o contexto do let
é bem claro, o in
é omitido.
import Data.Char
main = do
putStrLn "Digite seu nome: "
nome <- getLine
let nomeMaiúsculo = map toUpper nome
nomeMinúsculo = map toLower nome
putStrLn $ "Olá. Seu nome em letras maiúsculas é " ++ nomeMaiúsculo ++ " e em letras minúsculas é " ++ nomeMinúsculo
main
Até agora temos usado as funções que escrevemos apenas dentro do GHCi, de modo interpretado, o que demanda uma instalação do interpretador. Quando escrevemos um programa para ser executado em outras máquinas, normalmente queremos que ele seja um executável auto-contido, isto é, que execute independentemente de haver ou não um interpretador Haskell em tais máquinas. Para isso, devemos compilar o programa e gerar um arquivo executável.
Quando compilamos um programa Haskell, este deve ter uma função main
(em um módulo que dever explicitamente denominado Main
, caso tenha mais de um módulo), e que será sempre a função invocada quando o executável for invocado.
Como ações de E/S só podem ser executadas dentro de outras ações de E/S, a função main
deve ser uma ação E/S também.
main :: IO ()
main = do
putStrLn "Digite seu nome: "
nome <- getLine
putStrLn $ "Olá, " ++ nome
>> ghc --make main.hs
[1 of 1] Compiling Main ( main.hs, main.o )
Linking main ...
>> ./main
Digite seu nome:
Asdrubalino
Olá, Asdrubalino
do
é açúcar sintático
TODO
main = do print "lala"
print "lele"
print "lili"
main = print "lala" >> print "lele" >> print "lili"
Funções úteis
sequence_ :: [IO a] -> IO ()
sequence :: [IO a] -> IO [a]