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:

  1. (putStrLn . show) 2
  2. (putStrLn . show) (2 > 3)

Sequência de ações usando do

editar

Quando 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?")
Nota: Poderíamos explicar a fundo a notação do, mas alguns outros tópicos já devem ser de nosso conhecimento para entendermos seu funcionamento. Por hora, nos basta saber usá-la, pois ela nos permite criar programas completos, com ações externas. Não é recomendável, mas caso você quera se arriscar a entender do, a explicação está contida no capítulo Entendendo mônadas em diante.

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:

  1. putStrLn, que imprime uma mensagem na tela do terminal.
  2. getLine, que lê uma linha de texto digitada no terminal.
  3. 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.

  1. 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?"
    
  2. 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?"
    
  3. 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

editar

Outras 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 bloco do 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:

  1. A função read dentro da <colgroup></colgroup>ndição do primeiro if converte chute num dado que possa ser comparado com num.
  2. No primeiro then, inicia-se uma nova sequência de ações (um novo bloco do) contendo dois usos de putStrLn. Isso quer dizer que a última ação tem tipo IO () e, portanto, o tipo de final dos condicionais deve ser também IO ().
  3. No primeiro else, temos um if aninhado:
    1. Novamente, usa-se read para converter chute na comparação com num.
    2. No segundo then, temos outras duas ações de putStrLn, portanto, o tipo do bloco do é IO (). Isso satisfaz a condição de igualdade de tipos dentro de if.
    3. No último else, não há sequência de ações com um novo do, mas apenas um uso de putStrLn, que faz parte do bloco iniciado pelo do da primeira linha da função. O tipo final desta ação é, novamente, IO (), o que satisfaz todas as condições para se combinar blocos do e if ... then ... else ....
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

editar

Nã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

editar

Devemos 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:

  1. Extraímos o String de dentro de IO String, que é o resultado de getLine, usando <-.
  2. Usamos read texto :: Int para convertê-lo num num número do tipo Int.
  3. Associamos o resultado de read à variável numero usando <-.
  4. 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.