Leitura e escrita de dados em Haskell

Hoje vamos comentar sobre leitura e escrita de dados em Haskell. A minha principal referência é o Capítulo 7 do livro “Real World Haskell“.

Começamos com um exemplo bem simples que lê e escreve na saída padrão:

-- hello.hs
main = do
     putStrLn "Entre com seu nome:"
     inpStr <- getLine
     putStrLn ("Ola " ++ inpStr)

Para rodá-la podemos fazer :load hello.hs seguido de main no ghci ou runghc hello.hs no próprio terminal ou gerar um executável com ghc hello.hs.

Aqui já temos diversos conceitos novos. Primeiramente temos as funções de leitura e escrita, respectivamente getLine e putStrLn.

Vamos analisar a assinatura de tipos dessas funções:

putStrLn :: String -> IO ()
getLine :: IO String

Os tipos IO () e IO String representam uma ação de IO. Um jeito de interpretá-los é como sendo um invólucro (wrapper) para um tipo. Assim, IO String é um invólucro para o tipo String e IO () é um invólucro para um vazio.

Entrada e saída de dados são tarefas intrinsecamente não-puras, pois em uma chamada para leitura de dados (p.e. usando getLine) nem sempre retornará o mesmo resultado e uma escrita dos dados (p.e. usando putStrLn) possui efeitos colaterais externos.

Haskell sendo uma linguagem puramente funcional, faz uso de ações para poder lidar com tarefas não-puras. Futuramente, quando for falar sobre Mónades, pretendo explicar melhor sobre isso.

Seguindo com a interpretação do exemplo, temos o operador “<-“. Ele é uma maneira de executar uma ação, “extraindo” o seu conteúdo. Nesse caso, estamos “extraindo” a String lida com o getLine e atribuindo à variável inpStr.

Temos também o operador return, que encapsula um tipo em uma action, sendo o oposto do operador “<-” e não deve ser confundido com a palavra-chave de mesmo nome em linguagens imperativas.

O seguinte exemplo demonstra o encapsulamento de uma String em uma action, seguida da extração da mesma.

main = do
     inpStr <- return "Encapsulando e extraindo mensagem"
     putStrLn inpStr

O bloco definido pela palavra-chave do pode ser reescrito como uma sequência de operações ligadas por operadores do tipo “>>” e “>>=“. Analisando as assinaturas de tipos desses operadores temos:

(>>) :: (Monad m) => m a -> m b -> m b
(>>=) :: (Monad m) => m a -> (a -> m b) -> m b

Aqui Monad é uma typeclass. IO é uma instância de Monad. O operador “>>” serve para conectar duas ações, descartando o resultado da primeira e retornando a segunda.

main = 
     putStr "Ola" >> putStrLn " Mundo"

O operador “>>=” executa uma ação e alimenta uma função com o valor retornado. Essa função gera uma nova ação. Um exemplo:

main = 
     getLine >>= (\x -> putStrLn $ "Ola " ++ x)

Assim, o exemplo do início do post poderia ser reescrito sem o uso da palavra-chave do:

main =
     putStrLn "Entre com seu nome:" >> 
       getLine >>= 
         (\inpStr -> putStrLn $ "Ola " ++ inpStr)

Manipulação de arquivos de texto

A leitura e escrita de arquivos de texto em Haskell é bastante parecida com a de linguagens imperativas. Temos a função openFile

openFile :: FilePath -> IOMode -> IO Handle

que recebe o caminho do arquivo (uma string) e um modo de acesso (leitura, escrita, etc.)), e retorna um manipulador (handler) do arquivo (encapsulado em uma ação de IO).

Temos também as funções hGetLine que lê uma linha do manipulador, hPutStrLn que escreve uma linha no manipulador e hIsEOF que decide se o manipulador atingiu o final do arquivo.

import System.IO
import Data.Char(toUpper)

main :: IO()
main = do
     inh <- openFile "entrada.txt" ReadMode
     outh <- openFile "saida.txt" WriteMode     
     mainloop inh outh
     hClose inh
     hClose outh

mainloop inh outh = 
         do ineof <- hIsEOF inh
            if ineof
               then return()
               else do inpStr <- hGetLine inh
                       hPutStrLn outh (map toUpper inpStr)
                       mainloop inh outh

Temos os manipuladores para a entrada padrão, a saída padrão e a saída de erro. Desta forma poríamos usar as seguintes definições:

getLine = hGetLine stdin
putStrLn = hPutStrLn stdout
print = hPrint stdout

hGetContents

Ao invés de definir a função mainloop como no exemplo acima, podemos usar a função hGetContents

hGetContents :: Handle -> IO String

import System.IO
import Data.Char(toUpper)

main :: IO ()
main = do 
       inh <- openFile "entrada.txt" ReadMode
       outh <- openFile "saida.txt" WriteMode
       inpStr <- hGetContents inh
       hPutStr outh (map toUpper inpStr)
       hClose inh
       hClose outh

Como Haskell é uma linguagem com avaliação preguiçosa (lazy evaluation), no exemplo acima a função hGetContents não carregará todo o arquivo em memória, mas fará a leitura e escrita em partes.

readFile, writeFile e interact

As funções readFile e writeFile simplificam a leitura e escrita em arquivos de texto para casos como o do nosso exemplo, abstraindo o uso direto dos manipuladores:

readFile :: FilePath -> IO String
writeFile :: FilePath -> String -> IO ()

A nova versão fica bastante concisa:

import System.IO
import Data.Char(toUpper)

main :: IO ()
main = do 
       inpStr <- readFile "entrada.txt"
       writeFile "saida.txt" (map toUpper inpStr)

No caso particular de estarmos trabalhando com a entrada e saída padrão, podemos usar a função interact. Ela lê uma String da entrada padrão, processa com a função passada como parâmetro e retorna a String processada.

interact :: (String -> String) -> IO ()

Nesse caso nosso código se reduziria a uma linha!

import Data.Char(toUpper)

main :: IO ()
main =
       interact (map toUpper)

Buffering

Se rodarmos o código acima no ghci, ao mesmo tempo em que digitamos os caracteres eles passam a ser impressos no terminal. Há três tipos de buffers NoBuffering, LineBuffering e BlockBuffering. O modo padrão no ghci é o NoBuffering. Podemos mudar o modo padrão através da função hSetBuffering,

main :: IO ()
main = do
       hSetBuffering stdin LineBuffering
       interact (map toUpper)

Um efeito colateral indesejável é que depois que executarmos o main, os próximos comandos digitados no ghci só aparecerão depois de apertarmos o Enter :P

Parâmetros via linha de comando

Para obter os parâmetros passados como parâmetro via linha de comando, podemos usar a função getArgs presente em System.Environment

getArgs :: IO [String]

Ele retorna uma action contendo uma lista de strings correspondente aos parâmetros.

import System.IO
import System.Environment

main = do
     list <- getArgs
     putStrLn(show(list))

Praticando com problemas de programação

Agora que já aprendi o básico de leitura da entrada e saída padrão, vou começar a praticar no SPOJ-Br com os problemas da OBI. Até agora eu vinha usando o project Euler pois lá só precisa submeter a solução, não importando o modo como se faz a leitura/escrita dos dados.

Outra maneira de praticar é com a lista de 99 problemas de Haskell, mas esse eu ainda não tentei.

Minha sugestão de problemas para serem resolvidos no SPOJ-Br são Quadrados, Soma, Fatorial, Primo. Alguns com lógica bem simples também, mas um pouco mais chato de processar a entrada são Pneu, Fliperama, Garçom, Tacógrafo e Sedex.

Spoiler!

No problema Quadrados dá fazer uma solução bem compacta usando conceitos discutidos anteriormente como IO, funções lambda, currying e composição de função:

main = interact ((++ "\n") . show . (\x -> x*x) . read)

Referências

[1] Real World Haskell – Capítulo 7
[2] Haskell.org – Introduction to IO

2 respostas a Leitura e escrita de dados em Haskell

  1. marcelo da mata diz:

    Sensacional o seu blog Kunigame. Ainda não havia encontrado um como o seu em português.

%d bloggers like this: