Haskell/Entradas e saídas simples
Este módulo encontra-se em processo de tradução. A sua ajuda é bem vinda. |
Uma das funções mais importantes que um programa pode ter é interagir como o mundo exterior. A tarefa mais comum para iniciantes em qualquer linguagem de programação, geralmente é escrever "olá, mundo" na tela do computador. Eis a versão em Haskell, no GHCi:
Prelude> putStrLn "Olá, mundo!" "Olá, mundo!"
A função putStrLn
é uma das funções nativas de Haskell, e encontra-se no Prelude. Ela recebe um String com argumento e o imprime na tela.
Agora responda: qual é assinatura de tipo de putStrLn
? Sabemos que ela recebe um String, e a saída é a ação de imprimir um texto na tela. Também sabemos que toda saída de toda função em Haskell deve possuir um tipo. Então, qual seria o tipo da "ação de imprirmir"? Vejamos o que o GHCi nos diz:
Prelude> :t putStrLn putStrnLn :: String -> IO ()
Como interpretar IO ()
? Primeiro, devemos saber que "IO", em programação, geralmente quer dizer "input and output", ou "entrada e saída" em inglês. É exatamente isso que putStrLn
faz: ela retorna uma saída do programa para o mundo exterior. Quanto a ()
, imagine que seja uma n-upla de tamanho zero, a que chamamos de "unidade", em Haskell (unit, em inglês). Este tipo dado representa o retorno da ação de putStrLn
, ou seja, representa "nada". Em outras palavras, quer dizer que quando putStrLn
é executada, ela não retorna um valor ou dado que possa ser utilizado pelo programa. Ao invés disso, ela apenas imprime o texto na tela e apenas isso. O tipo de putStrLn
pode ser entendido como sendo "uma ação que retorna ()
".
Existem inúmeros exemplos IO num programa:
- Imprimir um texto na tela;
- Ler teclas pressionadas pelo usuário;
- Escrever dados num arquivo;
- Ler dados contidos num arquivo.
E como é que ações IO funcionam? Na verdade, este é um processo nada simples, pois acontecem muitas interação entre as muitas partes do computador (sistema operacional, drivers, hardware etc.) para que um texto seja finalmente exibido na tela, por exemplo. Felizmente, não precisamos nos preocupar com isso, além não ser esse o escopo deste livro. O que devemos saber, entretanto, é que todo programa em Haskell está contido numa grande função IO, chamada main
("principal", em inglês), cujo tipo é IO ()
, como veremos a seguir.
- Exercício
No capítulo Tipos básicos, mencionamos a função abrirJanela
de forma simplificada. Se pensarmos que toda ação realizada com o mundo externo é marcada por IO
, qual deveria ser o tipo de abrirJanela
?
As funções read
e show
editar
Antes de prosseguirmos, vamos introduzir duas funções bastante úteis: read
e show
. Ambas estão contidas no Prelude e, apesar de sequer realizarem entrada/saída, são bastante comuns dentro de funções IO
. Portanto, nos convém aprensetá-las neste capítulo.
A função read
(read em inglês significa "ler") converte um texto num dado de outro tipo. Veja seu tipo:
Prelude> :t read read :: Read a => String -> a
Trata-se de uma função cuja entrada é um String, e cuja saída é um dado polimórfico do tipo a
. A primeira parte é uma restrição de classe e quer dizer que read
só funciona se o dado de saída puder ser intepretado a partir de texto, isto é, se a
for da classe Read
.
O fato de a saída ser polimórfica é bastante conveniente, pois podemos forçar o tipo por conta própria, ou deixar que o GHC tente inferí-lo a partir do contexto. Por exemplo:
Prelude> let x = read "1" -- 'x' é polimórfico Prelude> let y :: Float ; y = read "2" -- 'y' é Float Prelude> :t x x :: Read a => a Prelude> :t y y :: Float Prelude> x + y -- GHCi deve inferir que 'x' é Float para poder somar 3.0 Prelude> :t (x + y) (x + y) :: Float
Vale notar que há duas formas de se escrever explicitamente o tipo da saída de read
:
-- O tipo de 'a' é definido a partir da expressão de dentro (a partir de 'read'):
a = (read "1" :: Float)
-- O tipo da saída de 'read' é definido a partir da anotação de fora (a partir de 'b'):
b :: Float
b = read "2"
A função show
(show em inglês significa "mostrar") é exatamente o oposto de read
: ela converte um dado para uma representação em texto. Seu tipo é:
Prelude> :t show show :: Show a => a -> String
Ela recebe um dado de qualquer tipo a
e retorna um dado String. Bem como em read
, também temos polimorfismo em show
, mas na entrada. Por isso não precisamos nos preocupar com o tipo do argumento a
. A restrição é que ele deve ser de um tipo que possa ser representado em texto (Show a
). A saída, por sua vez, é um String.
Seu uso é bastante simples, sendo muitas vezes empregada em conjunto com putStrLn
:
Prelude> show 2 "2" Prelude> show (2 > 3) "False" Prelude> (putStrLn . show) 2 2 Prelude> (putStrLn . show) (2 > 3) False
No exemplo acima, parece que não faz diferença usar ou não putStrLn
. Mas, na verdade, é porque estamos usando o GHCi, pois ele quase sempre mostra o resultado de uma expressão no terminal.
- Exercício
Explique por que não se pode usar putStrLn
sem ter aplicado show
nos seguintes exemplos:
(putStrLn . show) 2
(putStrLn . show) (2 > 3)
Sequência de ações usando do
editarQuando trabalhamos com funções que não realizam entrada/saída de dados, a sequência de computação do programa não importa tanto assim, desde que os resultados intermediários sejam direcionados corretamente ao longo da execução. Entretanto, num programa que realiza entrada/saída, não se sabe o estado de um sistema até o momento da execução e, portanto, é difícil prever o resultado das entradas e saídas. Para lidar com esta incerteza, é importante que todas as ações de entrada/saída sejam realizadas numa sequência previsível e predeterminada.
É exatamente isso que a notação do
faz: nos permite criar sequências de ações dentro de uma função IO
. Veja:
main :: IO ()
main = do
putStrLn "Digite seu nome, por favor:"
nome <- getLine
putStrLn ("Olá, " ++ nome ++ ". Como vai?")
Antes de entrendermos main
por completo, observe a função getLine
. Ela acessa o "mundo externo" para ler a linha digitada no terminal. Vejamos seu tipo:
Prelude> :t getLine getLine :: IO String
A assinatura de getLine
nos diz exatamente o que ela faz: uma ação de entrada/saída (IO
) que, quando executada, retorna um dado do tipo String
, o qual pode ser usado pelo programa posteriormente. Mas qual é o argumento de entrada de getLine
? Como já vimos, funções tem assinatura de tipo parecida com a -> b
, mas neste caso, a entrada de getLine
é a linha de texto do terminal, e não um dado fornecido pelo próprio. Assim, eliminamos o a
da assinatura de tipo.
Retornando ao exemplo, temos três ações:
putStrLn
, que imprime uma mensagem na tela do terminal.getLine
, que lê uma linha de texto digitada no terminal.putStrLn
, que, novamente, imprime um texto na tela do terminal.
Depois de imprimir o primeito texto, na segunda ação usamos <-
para designar o texto lido por getLine
à variável nome
. Como não sabemos qual será o texto digitado pelo usuário, podemos pelo menos definir uma variável que irá recebê-lo. Sempre que precisarmos usar o mesmo texto dentro do mesmo bloco do
, basta usar a variável nome
. Por último, nome
é concatenada a mais textos que serão impressos no terminal.
Perceba que main
não tem nenhum argumento. A única entrada acontece quando executamos getLine
, sendo que já explicamos seu tipo. Há, também, duas saídas, as quais acontecem quando executamos putStrLn
, mas que não retornam nenhum resultado para o programa. Portanto, se não há argumento de entrada, se a função realiza ações de entrada/saída, e se ela não retorna nenhum resultado, seu tipo deve ser IO ()
, como já estava anotado.
Sobre o uso de <-
editar
Mesmo que sejam criadas variáveis dentro de um programa, nem sempre somos obrigados a usá-las. Além do mais, dentro de uma função IO
, <-
pode ser usada em qualquer ação, exceto a última.
- Um caso em que não criamos variável e, portanto, não usamos o resultado de
getLine
, apesar deste comando ser executado mesmo assim:main = do putStrLn "Digite seu nome:" getLine putStrLn "Como vai?"
- Um caso em que criamos duas variáveis, mas ambas não são usadas:
main = do x <- putStrLn "Digite seu nome:" nome <- getLine putStrLn "Olá. Como vai?"
- Um caso em que a última ação é uma associação usando
<-
e, portanto, é inválido:main = do x <- putStrLn "Digite seu nome:" nome <- getLine y <- putStrLn ("Olá, " ++ nome ++ ". Como vai?")
No exemplo 3, teríamos um erro do compilador:
EntradaESaida.hs:5:2: The last statement in a 'do' construct must be an expression y <- putStrLn ("Olá, " ++ nome ++ ". Como vai?")
Esta é exatamente a restrição que explicamos no começo da subseção. Infelizmente, também não faz parte do escopo deste capítulo explicar a motivação desta restrição.
Outra função de <-
é extrair o dado de tipo a
de dentro de IO a
. Veja a seguinte linha:
nome <- getLine
Se pudéssemos usar :t para descobrir tipo de nome
, ele seria String.
. Como já sabemos, getLine
é uma função sem argumentos que retorna um dado do tipo IO String
. Ao associar seu resultado a uma variável usando <-
, o valor do tipo String
é extraído de dentro de IO String
. Assim, ele pode ser usado com um String
normal. Abra o GHCi e teste por você mesmo:
Prelude> putStrLn getLine
Teremos um erro dizendo que IO String
não é um [Char]
(que é sinônimo de String
). Entretanto, se fizermos como no exemplo inicial, não teremos nenhum problema.
- Exercício
Escreva um programa que peça para o usuário digitar a base e a altura de um triângulo e que calcule sua área. Ele deve apresentar o resultado na tela. Um exemplo seria:
A base? 3.3 A altura? 5.4 A área do triângulo é 8.91.Você deve vai precisar usar as funções
read
e show
para converter o texto do usário em número, e depois o resultado numérico em texto.
Controlando as ações
editarOutras construções em Haskell também podem ser usadas dentro de blocos do
. Podemos, inclusive, usar do
aninhados, mas alguns cuidados devem ser tomados. Como vimos no capítulo passado, expressões if possuem restrições de tipo. Tais restrições também devem ser aplicadas quando usamos do
dentro de then e else. Isso quer dizer que:
- O bloco
do
dentro de then deve ter o mesmo tipo final que o blocodo
dentro de else. - Por ser um bloco
do
, a última ação de then ou else não pode ser uma associação de variável usando<-
.
Vejamos um simples programa de "advinho o número":
adivinhe num = do
putStrLn "Digite um número:"
chute <- getLine
if (read chute) < num
then do putStrLn "Muito baixo!"
putStrLn "Tente novamente!"
else if (read chute) > num
then do putStrLn "Muit alto!"
putStrLn "Tente novamente!"
else putStrLn "Você acertou!"
No caso da função adivinhe
, temos:
- A função
read
dentro da <colgroup></colgroup>ndição do primeiro if convertechute
num dado que possa ser comparado comnum
. - No primeiro then, inicia-se uma nova sequência de ações (um novo bloco
do
) contendo dois usos deputStrLn
. Isso quer dizer que a última ação tem tipoIO ()
e, portanto, o tipo de final dos condicionais deve ser tambémIO ()
. - No primeiro else, temos um if aninhado:
- Novamente, usa-se
read
para converterchute
na comparação comnum
. - No segundo then, temos outras duas ações de
putStrLn
, portanto, o tipo do blocodo
éIO ()
. Isso satisfaz a condição de igualdade de tipos dentro de if. - No último else, não há sequência de ações com um novo
do
, mas apenas um uso deputStrLn
, que faz parte do bloco iniciado pelodo
da primeira linha da função. O tipo final desta ação é, novamente,IO ()
, o que satisfaz todas as condições para se combinar blocosdo
e if ... then ... else ....
- Novamente, usa-se
- Exercício
Escreva um programa que pergunta o nome do usuário. Se o nome for Carlos ou Pedro, o programa responde que Haskell é uma ótima linguagem de programação. Se for Maria, o programa responde que Haskell pode ser aprendido por qualquer um. Se for qualquer outra resposta, o programa responde que não conhece o usuário. Escreva pelo menos uma solução usando if ... then ... else ....
Encontrando erros
editarÀ primeira vista, usar blocos do
parece ser bastante simples, mas eles acabam sendo fonte de muita frustração para muitos inciantes em Haskell. Se você encontrar algum erro quando estiver programando, releia este capítulo. É possível que a explicação esteja contida aqui. Abaixo temos um resumo simplificado dos principais erros cometidos por iniciantes.
Atenção quanto aos tipos dos dados
editarNão tem simplifica seu programa eliminando variáveis. Já vimos um exemplo similar:
main = do putStrLn "Qual o seu nome?"
putStrLn ("Olá, " ++ getLine)
Temos um erro de tipo, pois getLine
retorna um dado cujo tipo é IO String
. Devemos, portanto, usar <-
para extrair o String
:
main = do putStrLn "Qual o seu nome?"
nome <- getLine
putStrLn ("Olá, " ++ nome)
Atenção quanto aos tipos das associações
editarDevemos ter cuidado ao criar variáveis dentro de blocos do
, pois <-
só funciona para extrair dados. Caso o valor já seja um dado puro, não podemos associá-lo a uma variável. O exemplo a seguir resulta num erro:
main = do putStrLn "Digite um número inteiro:"
texto <- getLine
numero <- (read texto :: Int)
putStrLn ("O dobro do seu número é: " ++ (show (2 * numero)))
Vamos interpretar o programa:
- Extraímos o
String
de dentro deIO String
, que é o resultado degetLine
, usando<-
. - Usamos
read texto :: Int
para convertê-lo num num número do tipoInt
. - Associamos o resultado de
read
à variávelnumero
usando<-
. - Apresentamos o dobro do número digitado pelo usuário.
A falha encontra-se na terceira etapa: não há como usar <-
para associar um Int
a uma variável dentro de um bloco do
. Como já dissemos, sua função é extrair valores de dentro de IO
. Porém, Int
já é, por si só, um dado puro e, portanto, não há o que extrair dali.
Uma possível solução seria usar where
para definir uma função interna:
main = do putStrLn "Digite um número: "
texto <- getLine
putStrLn ("O dobro do seu número é: " ++ (show . dobrar) texto)
where dobrar x = 2 * (read x :: Int)
Outra solução é usar let
:
sem usar in
|
usando in
|
---|---|
main = do putStrLn "Digite um número: "
texto <- getLine
let dobro = 2 * (read texto :: Int)
putStrLn ("O dobro do seu número é: " ++ (show dobro))
|
main = do putStrLn "Digite um número: "
texto <- getLine
let dobro = 2 * (read texto :: Int) in
do putStrLn ("O dobro do seu número é: " ++ (show dobro))
|
As duas opção são válidas, pois dentro de um bloco do
temos a opção de usar ou não in
.