Entrada e Saída (Input and Output)

Já mencionamos que Haskell é uma linguagem puramente funcional. Considerando que, em linguagens imperativas, você geralmente faz as coisas dando ao computador uma série de etapas para executar, a programação funcional é mais sobre definir o que as coisas são. Em Haskell, uma função não pode alterar algum estado, como alterar o conteúdo de uma variável (quando uma função muda de estado, dizemos que a função tem efeitos colaterais ou side-effects). A única coisa que uma função pode fazer em Haskell é nos devolver algum resultado com base nos parâmetros que demos a ela. Se uma função for chamada duas vezes com os mesmos parâmetros, ela deverá retornar o mesmo resultado. Embora isso possa parecer um pouco limitador quando você vem de um mundo imperativo, vimos que é realmente muito legal. Em uma linguagem imperativa, você não tem garantia de que uma função simples que deve apenas processar alguns números não queimará sua casa, sequestrará seu cachorro e arranhará seu carro com uma batata enquanto processa esses números. Por exemplo, quando estávamos fazendo uma árvore de busca binária, não inserimos um elemento em uma árvore modificando alguma árvore no lugar. Nossa função para inserir em uma árvore de busca binária realmente retornou uma nova árvore, porque não pode mudar a antiga.
Embora as funções serem incapazes de mudar o estado seja bom, porque nos ajuda a raciocinar sobre nossos programas, há um problema com isso. Se uma função não pode mudar nada no mundo, como é que ela deve nos dizer o que calculou? Para nos dizer o que calculou, ela tem que mudar o estado de um dispositivo de saída (geralmente o estado da tela), que então emite fótons que viajam para o nosso cérebro e mudam o estado de nossa mente, cara.
Não se desespere, nem tudo está perdido. Acontece que Haskell realmente tem um sistema muito inteligente para lidar com funções que têm efeitos colaterais que separam perfeitamente a parte de nosso programa que é pura e a parte do nosso programa que é impura, que faz todo o trabalho sujo, como conversar com o teclado e a tela. Com essas duas partes separadas, ainda podemos raciocinar sobre nosso programa puro e tirar proveito de todas as coisas que a pureza oferece, como preguiça, robustez e modularidade, enquanto nos comunicamos eficientemente com o mundo exterior.
Olá, mundo! (Hello, world!)

Até agora, sempre carregamos nossas funções no GHCI para testá-las e
brincar com elas. Também exploramos as funções da biblioteca padrão
dessa maneira. Mas agora, depois de oito ou mais capítulos, finalmente
escreveremos nosso primeiro programa Haskell real! Yay! E com
certeza, vamos fazer o bom e velho esquema
"hello, world".
Ei! Para os fins deste capítulo, vou assumir que você está usando um ambiente unix-y para aprender Haskell. Se você estiver no Windows, sugiro que faça o download do Cygwin, que é um ambiente semelhante ao Linux para Windows, A.k.A. exatamente o que você precisa.
Portanto, para começar, coloque o seguinte em seu editor de texto favorito:
main = putStrLn "hello, world"
Acabamos de definir um nome chamado main e nele chamamos
uma função chamada putStrLn com o parâmetro
"hello, world". Parece bastante comum, mas não é, como
veremos em apenas alguns instantes. Salve esse arquivo como
helloworld.hs.
E agora, vamos fazer algo que nunca fizemos antes. Na verdade, vamos
compilar nosso programa! Estou tão animado! Abra seu terminal e navegue
até o diretório onde helloworld.hs está localizado e faça o
seguinte:
$ ghc --make helloworld
[1 of 1] Compiling Main ( helloworld.hs, helloworld.o )
Linking helloworld ...
Ok! Com alguma sorte, você conseguiu algo assim e agora pode executar
seu programa fazendo ./helloworld.
$ ./helloworld
hello, world
E lá vamos nós, nosso primeiro programa compilado que imprimiu algo no terminal. Quão extraordinariamente chato!
Vamos examinar o que escrevemos. Primeiro, vamos ver o tipo da função
putStrLn.
ghci> :t putStrLn
putStrLn :: String -> IO ()
ghci> :t putStrLn "hello, world"
putStrLn "hello, world" :: IO ()
Podemos ler o tipo de putStrLn assim:
putStrLn pega uma string e retorna uma ação de E/S
(I/O action) que tem um tipo de resultado de ()
(ou seja, a tupla vazia, também conhecida como unit). Uma
ação de E/S é algo que, quando realizado, realizará uma ação com um
efeito colateral (geralmente lendo da entrada ou imprimindo coisas na
tela) e também conterá algum tipo de valor de retorno dentro dele. A
impressão de uma string no terminal realmente não possui nenhum tipo de
valor de retorno significativo, portanto, um valor fictício de
() é usado.
A tupla vazia é um valor de () e também tem um tipo de
().
Então, quando uma ação de E/S será executada? Bem, é aqui que
main entra. Uma ação de E/S será realizada quando dermos um
nome de main e executarmos nosso programa.
Ter todo o seu programa sendo apenas uma ação de E/S parece meio limitador. É por isso que podemos usar a sintaxe do para colar várias ações de E/S em uma. Dê uma olhada no seguinte exemplo:
main = do
putStrLn "Hello, what's your name?"
name <- getLine
putStrLn ("Hey " ++ name ++ ", you rock!")
Ah, interessante, nova sintaxe! E isso parece muito com um programa
imperativo. Se você compilá-lo e testá-lo, provavelmente se comportará
exatamente como você espera. Observe que dissemos do e depois
definimos uma série de etapas, como faríamos em um programa imperativo.
Cada uma dessas etapas é uma ação de E/S. Ao juntá-los com a sintaxe
do, nós os colamos em uma ação de E/S. A ação que obtivemos tem
um tipo de IO (), porque esse é o tipo da última ação de
E/S dentro.
Por esse motivo, main sempre tem uma assinatura de tipo
de main :: IO something, onde
something é algum tipo concreto. Por convenção,
geralmente não especificamos uma declaração de tipo para
main.
Uma coisa interessante que não encontramos antes é a terceira linha,
que afirma name <- getLine. Parece que lê uma linha da
entrada e a armazena em uma variável chamada name.
Realmente? Bem, vamos examinar o tipo de getLine.
ghci> :t getLine
getLine :: IO String

Aha, o-kay. getLine é uma ação de E/S que contém um tipo
de resultado de String. Isso faz sentido, porque aguardará
o usuário inserir algo no terminal e, em seguida, esse algo será
representado como uma string. Então, o que há com
name <- getLine então? Você pode ler esse pedaço de
código assim: execute a ação de E/S getLine e
vincule seu valor de resultado a name.
getLine tem um tipo de IO String, então
name terá um tipo de String. Você pode pensar
em uma ação de E/S como uma caixa com pezinhos que sairão para o mundo
real e farão algo lá (como escrever algum grafite em uma parede) e
talvez trazer de volta alguns dados. Depois de buscar esses dados para
você, a única maneira de abrir a caixa e obter os dados dentro dela é
usar a construção <-. E se estamos tirando dados de uma
ação de E/S, só podemos retirá-los quando estamos dentro de outra ação
de E/S. É assim que Haskell consegue separar perfeitamente as partes
puras e impuras do nosso código. getLine é, em certo
sentido, impuro porque seu valor de resultado não é garantido ser o
mesmo quando executado duas vezes. É por isso que é uma espécie de
contaminado (tainted) com o construtor de tipo
IO e só podemos extrair esses dados no código de E/S. E
como o código de E/S também está contaminado, qualquer computação que
depende de dados de E/S contaminados terá um resultado contaminado.
Quando digo contaminado, não quero dizer contaminado de tal
maneira que nunca possamos usar o resultado contido em uma ação de E/S
novamente em código puro. Não, nós temporariamente
descontaminamos os dados dentro de uma ação de E/S quando os
vinculamos a um nome. Quando fazemos name <- getLine,
name é apenas uma string normal, porque representa o que
está dentro da caixa. Podemos ter uma função realmente complicada que,
digamos, leva seu nome (uma string normal) como um parâmetro e diz sua
sorte e todo o futuro da sua vida com base em seu nome. Nós podemos
fazer isso:
main = do
putStrLn "Hello, what's your name?"
name <- getLine
putStrLn $ "Read this carefully, because this is your future: " ++ tellFortune name
e tellFortune (ou qualquer uma das funções para as quais
passa name) não precisa saber nada sobre E/S, é apenas uma
função normal String -> String!
Dê uma olhada neste trecho de código. É válido?
nameTag = "Hello, my name is " ++ getLine
Se você disse não, vá comer um biscoito. Se você disse sim, beba uma
tigela de lava derretida. Brincadeira, não! A razão pela qual isso não
funciona é que ++ exige que seus dois parâmetros sejam
listas sobre o mesmo tipo. O parâmetro esquerdo tem um tipo de
String (ou [Char], se quiser), enquanto
getLine tem um tipo de IO String. Você não
pode concatenar uma string e uma ação de E/S. Primeiro temos que tirar o
resultado da ação de E/S para obter um valor do tipo String
e a única maneira de fazer isso é dizer algo como
name <- getLine dentro de outra ação de E/S. Se queremos
lidar com dados impuros, temos que fazê-lo em um ambiente impuro.
Portanto, a mancha da impureza se espalha muito como o flagelo dos
mortos-vivos (undead scourge) e é do nosso interesse manter as partes de
E/S do nosso código o menor possível.
Toda ação de E/S que é executada tem um resultado encapsulado nela. É por isso que nosso programa de exemplo anterior também poderia ter sido escrito assim:
main = do
foo <- putStrLn "Hello, what's your name?"
name <- getLine
putStrLn ("Hey " ++ name ++ ", you rock!")
No entanto, foo teria apenas um valor de
(), portanto, fazer isso seria meio inútil. Observe que não
vinculamos o último putStrLn a nada. Isso ocorre porque em
um bloco do, a última ação não pode ser vinculada a um
nome como os dois primeiros foram. Veremos exatamente por que
isso é um pouco mais tarde, quando nos aventurarmos no mundo das
mônadas. Por enquanto, você pode pensar nisso da maneira que o bloco
do extrai automaticamente o valor da última ação e o liga ao
seu próprio resultado.
Exceto a última linha, todas as linhas em um bloco do que
não se ligam também podem ser escritas com um bind. Então
putStrLn "BLAH" pode ser escrito como
_ <- putStrLn "BLAH". Mas isso é inútil, então deixamos
o <- para ações de E/S que não contêm um resultado
importante, como putStrLn something.
Os iniciantes às vezes pensam que fazer
name = getLine
lerá a entrada e depois ligará o valor de name. Bem,
isso não acontecerá, tudo o que isso faz é dar à ação de E/S
getLine um nome diferente chamado, bem, name.
Lembre-se, para obter o valor de uma ação de E/S, você deve executá-la
dentro de outra ação de E/S, ligando-a a um nome com
<-.
As ações de E/S serão executadas apenas quando receberem um nome de
main ou quando estiverem dentro de uma ação de E/S maior
que compusemos com um bloco do. Também podemos usar um bloco
do para colar algumas ações de E/S e, em seguida, podemos usar
essa ação de E/S em outro bloco do e assim por diante. De
qualquer maneira, elas serão executadas apenas se eventualmente caírem
no main.
Ah, certo, também há mais um caso em que as ações de E/S serão executadas. Quando digitamos uma ação de E/S no GHCI e pressionamos enter, ela será executada.
ghci> putStrLn "HEEY"
HEEY
Mesmo quando apenas digitamos um número ou chamamos uma função no
GHCI e pressionamos enter, ele o avaliará (tanto quanto necessário) e,
em seguida, chamará show nele e depois imprimirá essa
string no terminal usando putStrLn implicitamente.
Lembra das ligações let? Se você não, atualize sua memória
sobre elas lendo esta
seção. Elas precisam ter a forma de let bindings in
expression, onde bindings são
nomes a serem dados às expressões e expression é a
expressão que deve ser avaliada que as vê. Também dissemos que, nas
compreesões de lista, a parte in não é necessária. Bem, você
pode usá-las em blocos do exatamente como você as usa nas
compreensões de lista. Verifique isso:
import Data.Char
main = do
putStrLn "What's your first name?"
firstName <- getLine
putStrLn "What's your last name?"
lastName <- getLine
let bigFirstName = map toUpper firstName
bigLastName = map toUpper lastName
putStrLn $ "hey " ++ bigFirstName ++ " " ++ bigLastName ++ ", how are you?"
Veja como as ações de E/S no bloco do estão alinhadas?
Observe também como o let está alinhado com as ações de E/S e
os nomes do let estão alinhados um com o outro? Essa é uma boa
prática, porque a indentação é importante em Haskell. Agora, fizemos
map toUpper firstName, o que transforma algo como
"John" em uma string muito mais legal como
"JOHN". Vinculamos essa string maiúscula a um nome e depois
a usamos em uma string posteriormente impressa no terminal.
Você deve estar se perguntando quando usar <- e
quando usar let bindings? Bem, lembre-se, <- é
(por enquanto) para executar ações de E/S e vincular seus resultados a
nomes. map toUpper firstName, no entanto, não é uma ação de
E/S. É uma expressão pura em Haskell. Então use <-
quando quiser vincular os resultados das ações de E/S aos nomes e você
pode usar let bindings para ligar expressões puras aos nomes.
Se tivéssemos feito algo como let firstName = getLine,
teríamos apenas chamado a ação de E/S getLine de um nome
diferente e ainda teríamos que executá-la através de um
<- para executá-la.
Agora vamos fazer um programa que lê continuamente uma linha e imprime a mesma linha com as palavras invertidas. A execução do programa parará quando inserirmos uma linha em branco. Este é o programa:
main = do
line <- getLine
if null line
then return ()
else do
putStrLn $ reverseWords line
main
reverseWords :: String -> String
reverseWords = unwords . map reverse . words
Para sentir o que ele faz, você pode executá-lo antes de examinarmos o código.
Dica profissional: Para executar um programa, você
pode compilá-lo e executar o arquivo executável produzido fazendo
ghc --make helloworld e depois ./helloworld ou
pode usar o comando runhaskell assim:
runhaskell helloworld.hs e seu programa será executado em
tempo real (on the fly).
Primeiro, vamos dar uma olhada na função reverseWords. É
apenas uma função normal que pega uma string como
"hey there man" e depois chama words com ela
para produzir uma lista de palavras como
["hey","there","man"]. Em seguida, mapeamos
reverse na lista, obtendo
["yeh","ereht","nam"] e depois colocamos isso de volta em
uma string usando unwords e o resultado final é
"yeh ereht nam". Veja como usamos a composição de funções
aqui. Sem composição de funções, teríamos que escrever algo como
reverseWords st = unwords (map reverse (words st)).
E sobre o main? Primeiro, recebemos uma linha do
terminal executando getLine e chamamos essa linha de
line. E agora, temos uma expressão condicional. Lembre-se
de que em Haskell, todo if deve ter um else
correspondente, porque toda expressão precisa ter algum tipo de valor.
Fazemos o if para que, quando uma condição seja verdadeira (no
nosso caso, a linha em que entramos está em branco), executamos uma ação
de E/S e, quando não é, a ação de E/S no else é executada. É
por isso que em um bloco do de E/S, ifs devem ter uma
forma de if condition then I/O action else
I/O action.
Vamos primeiro dar uma olhada no que acontece sob a cláusula else. Porque, temos que ter exatamente uma ação de E/S após o else, usamos um bloco do para colar duas ações de E/S em uma. Você também pode escrever essa parte como:
else (do
putStrLn $ reverseWords line
main)
Isso torna mais explícito que o bloco do pode ser visto como
uma ação de E/S, mas é mais feio. De qualquer forma, dentro do bloco
do, chamamos reverseWords na linha que obtivemos
de getLine e depois imprimimos no terminal. Depois disso,
apenas executamos main. É chamado recursivamente e tudo
bem, porque main é uma ação de E/S. Então, em certo
sentido, voltamos ao início do programa.
Agora, o que acontece quando null line é verdadeiro? O
que está depois do then é realizado nesse caso. Se olharmos
para cima, veremos que diz then return (). Se você já fez
linguagens imperativas como C, Java ou Python, provavelmente está
pensando que sabe o que esse return faz e é provável que
você já tenha pulado esse parágrafo realmente longo. Bem, aqui está:
o return em Haskell não é nada como o
return na maioria das outras linguagens! Tem o
mesmo nome, o que confunde muitas pessoas, mas na realidade é bem
diferente. Em linguagens imperativas, return geralmente
encerra a execução de um método ou sub-rotina e faz com que ele relate
algum tipo de valor a quem o chamou. Em Haskell (em ações de E/S
especificamente), ele faz uma ação de E/S a partir de um valor puro. Se
você pensar na analogia da caixa de antes, é preciso um valor e a
embrulha em uma caixa. A ação de E/S resultante não faz nada, apenas tem
esse valor encapsulado como seu resultado. Portanto, em um contexto de
E/S, return "haha" terá um tipo de IO String.
Qual é o ponto de apenas transformar um valor puro em uma ação de E/S
que não faz nada? Por que incomodar nosso programa com IO
mais do que é necessário? Bem, precisávamos de alguma ação de E/S para
realizar no caso de uma linha de entrada vazia. É por isso que acabamos
de fazer uma ação de E/S falsa que não faz nada escrevendo
return ().
O uso do return não faz com que o bloco do de
E/S termine em execução ou algo assim. Por exemplo, este programa
realizará tudo alegremente até a última linha:
main = do
return ()
return "HAHAHA"
line <- getLine
return "BLAH BLAH BLAH"
return 4
putStrLn line
Tudo o que esses returns fazem é criar ações de E/S que
realmente não fazem nada, exceto ter um resultado encapsulado e esse
resultado é jogado fora porque não está vinculado a um nome. Podemos
usar return em combinação com <- para
vincular coisas a nomes.
main = do
a <- return "hell"
b <- return "yeah!"
putStrLn $ a ++ " " ++ b
Então, como você vê, return é o oposto de
<-. Enquanto return pega um valor e o
envolve em uma caixa, <- pega uma caixa (e a executa) e
tira o valor dela, ligando-o a um nome. Mas fazer isso é meio
redundante, especialmente porque você pode usar let bindings em
blocos do para vincular a nomes, assim:
main = do
let a = "hell"
b = "yeah"
putStrLn $ a ++ " " ++ b
Ao lidar com blocos do de E/S, usamos principalmente
return porque precisamos criar uma ação de E/S que não faça
nada ou porque não queremos a ação de E/S composta de um bloco
do tenha o valor do resultado de sua última ação, mas queremos
que ele tenha um valor de resultado diferente; portanto, usamos
return para fazer uma ação de E/S que sempre tem o
resultado desejado contido e a colocamos no final.
Um bloco do também pode ter apenas uma ação de E/S. Nesse
caso, é o mesmo que apenas escrever a ação de E/S. Algumas pessoas
preferem escrever then do return () nesse caso, porque o
else também tem um do.
Antes de passarmos para arquivos, vamos dar uma olhada em algumas funções úteis ao lidar com E/S.
putStr é muito parecido com
putStrLn, pois é preciso uma string como um parâmetro e
retorna uma ação de E/S que imprimirá essa string no terminal, apenas
putStr não pula para uma nova linha depois de imprimir a
string enquanto putStrLn faz.
main = do putStr "Hey, "
putStr "I'm "
putStrLn "Andy!"
$ runhaskell putstr_test.hs
Hey, I'm Andy!
Sua assinatura de tipo é putStr :: String -> IO (),
portanto, o resultado encapsulado na ação de E/S resultante é a unit. Um
valor inútil, por isso não faz sentido vinculá-lo.
putChar pega um caractere e
retorna uma ação de E/S que o imprimirá no terminal.
main = do putChar 't'
putChar 'e'
putChar 'h'
$ runhaskell putchar_test.hs
teh
putStr é realmente definida recursivamente com a ajuda
de putChar. A condição de borda de putStr é a
string vazia; portanto, se estivermos imprimindo uma string vazia, basta
retornar uma ação de E/S que não faz nada usando return ().
Se não estiver vazia, imprima o primeiro caractere da string fazendo
putChar e depois imprima o resto deles usando
putStr.
putStr :: String -> IO ()
putStr [] = return ()
putStr (x:xs) = do
putChar x
putStr xs
Veja como podemos usar a recursão na E/S, exatamente como podemos usá-la no código puro. Assim como no código puro, definimos o caso de borda e, em seguida, pensamos qual é realmente o resultado. É uma ação que primeiro produz o primeiro caractere e depois produz o restante da string.
print pega um valor de qualquer
tipo que seja uma instância de Show (o que significa que
sabemos como representá-lo como uma string), chama show com
esse valor para “stringificá-lo” e, em seguida, envia essa string para o
terminal. Basicamente, é apenas putStrLn . show. Ele
primeiro executa show em um valor e depois alimenta isso
para putStrLn, que retorna uma ação de E/S que imprimirá
nosso valor.
main = do print True
print 2
print "haha"
print 3.2
print [3,4,3]
$ runhaskell print_test.hs
True
2
"haha"
3.2
[3,4,3]
Como você pode ver, é uma função muito útil. Lembra de como falamos
sobre como as ações de E/S são executadas apenas quando caem em
main ou quando tentamos avaliá-las no prompt do GHCI?
Quando digitamos um valor (como 3 ou [1,2,3])
e pressionamos a tecla de retorno, o GHCI realmente usa
print nesse valor para exibi-lo em nosso terminal!
ghci> 3
3
ghci> print 3
3
ghci> map (++"!") ["hey","ho","woo"]
["hey!","ho!","woo!"]
ghci> print (map (++"!") ["hey","ho","woo"])
["hey!","ho!","woo!"]
Quando queremos imprimir strings, geralmente usamos
putStrLn porque não queremos as aspas ao redor delas, mas
para imprimir valores de outros tipos no terminal, print é
a mais usada.
getChar é uma ação de E/S que lê
um caractere da entrada. Assim, sua assinatura de tipo é
getChar :: IO Char, porque o resultado contido na ação de
E/S é um Char. Observe que, devido ao buffer, a leitura dos
caracteres não acontecerá até que o usuário esmague a tecla de retorno
(Enter).
main = do
c <- getChar
if c /= ' '
then do
putChar c
main
else return ()
Este programa parece que deve ler um caractere e depois verificar se é um espaço. Se for, interrompa a execução e, se não, imprima-o no terminal e faça a mesma coisa tudo de novo. Bem, meio que faz, mas não da maneira que você pode esperar. Verifique isso:
$ runhaskell getchar_test.hs
hello sir
hello
A segunda linha é a entrada. Nós inserimos hello sir e
depois pressionamos return. Devido ao buffer, a execução do programa
começará somente após termos pressionado return e não após cada
caractere inserido. Mas assim que pressionamos o return, ele atua sobre
o que temos colocado até agora. Tente brincar com este programa para
sentir isso!
A função when é encontrada em
Control.Monad (para obter acesso a ela, faça
import Control.Monad). É interessante porque em um bloco
do parece uma instrução de fluxo de controle, mas na verdade é
uma função normal. Leva um valor booleano e uma ação de E/S, se esse
valor booleano for True, ele retornará a mesma ação de E/S
que fornecemos a ela. No entanto, se for False, ele
retornará a ação return (), ou seja, uma ação de E/S que
não faz nada. Aqui está como poderíamos reescrever o pedaço de código
anterior com o qual demonstramos getChar usando
when:
import Control.Monad
main = do
c <- getChar
when (c /= ' ') $ do
putChar c
main
Portanto, como você pode ver, é útil para encapsular o padrão
if something then do some I/O action else return
().
sequence uma lista de ações de
E/S e retorna uma ação de E/S que executará essas ações uma após a
outra. O resultado contido nessa ação de E/S será uma lista dos
resultados de todas as ações de E/S que foram executadas. Sua assinatura
de tipo é sequence :: [IO a] -> IO [a]. Fazendo
isso:
main = do
a <- getLine
b <- getLine
c <- getLine
print [a,b,c]
É exatamente o mesmo que fazer isso:.
main = do
rs <- sequence [getLine, getLine, getLine]
print rs
Então sequence [getLine, getLine, getLine] cria uma ação
de E/S que executará getLine três vezes. Se vincularmos
essa ação a um nome, o resultado será uma lista de todos os resultados,
portanto, no nosso caso, uma lista de três coisas que o usuário inseriu
no prompt.
Um padrão comum com sequence é quando mapeamos funções
como print ou putStrLn sobre listas. Fazer
map print [1,2,3,4] não criará uma ação de E/S. Ele criará
uma lista de ações de E/S, porque é como escrever
[print 1, print 2, print 3, print 4]. Se queremos
transformar essa lista de ações de E/S em uma ação de E/S, temos que
sequenciá-la.
ghci> sequence (map print [1,2,3,4,5])
1
2
3
4
5
[(),(),(),(),()]
O que há com [(),(),(),(),()] no fim? Bem, quando
avaliamos uma ação de E/S no GHCI, ela é executada e, em seguida, seu
resultado é impresso, a menos que esse resultado seja (),
caso em que não é impresso. É por isso que avaliar
putStrLn "hehe" no GHCI apenas imprime hehe
(porque o resultado contido em putStrLn "hehe" é
()). Mas quando fazemos getLine no GHCI, o
resultado dessa ação de E/S é impresso, porque getLine tem
um tipo de IO String.
Como o mapeamento de uma função que retorna uma ação de E/S sobre uma
lista e depois sequenciá-la é tão comum, as funções utilitárias mapM e mapM_ foram introduzidas.
mapM pega uma função e uma lista, mapeia a função sobre a
lista e depois a sequencia. mapM_ faz o mesmo, apenas joga
fora o resultado mais tarde. Geralmente usamos mapM_ quando
não nos importamos com o resultado de nossas ações sequenciadas de
E/S.
ghci> mapM print [1,2,3]
1
2
3
[(),(),()]
ghci> mapM_ print [1,2,3]
1
2
3
forever pega uma ação de E/S e
retorna uma ação de E/S que apenas repete a ação de E/S que recebeu para
sempre. Está localizada em Control.Monad. Este pequeno
programa pedirá indefinidamente ao usuário alguma entrada e cuspirá de
volta para ele, em CAPSLOCK:
import Control.Monad
import Data.Char
main = forever $ do
putStr "Give me some input: "
l <- getLine
putStrLn $ map toUpper l
forM (localizada em
Control.Monad) é como mapM, só que tem seus
parâmetros trocados. O primeiro parâmetro é a lista e a segunda é a
função a ser mapeada sobre essa lista, que é então sequenciada. Por que
isso é útil? Bem, com algum uso criativo de lambdas e notação
do, podemos fazer coisas assim:
import Control.Monad
main = do
colors <- forM [1,2,3,4] (\a -> do
putStrLn $ "Which color do you associate with the number " ++ show a ++ "?"
color <- getLine
return color)
putStrLn "The colors that you associate with 1, 2, 3 and 4 are: "
mapM putStrLn colors
O (\a -> do ... ) é uma função que pega um número e
retorna uma ação de E/S. Temos que cercá-lo com parênteses, caso
contrário, o lambda acha que as duas últimas ações de E/S pertencem a
ela. Observe que fazemos return color no bloco do
interno. Fazemos isso para que a ação de E/S que o bloco do
define tenha o resultado de nossa cor contida nele. Na verdade, não
tivemos que fazer isso, porque getLine já tem isso contido
nele. Fazer color <- getLine e depois
return color é apenas desembalar o resultado de
getLine e reempacotá-lo novamente, portanto é o mesmo que
apenas fazer getLine. O forM (chamado com seus
dois parâmetros) produz uma ação de E/S, cujo resultado nos vinculamos a
colors. colors é apenas uma lista normal que
contém strings. No final, imprimimos todas essas cores fazendo
mapM putStrLn colors.
Você pode pensar em forM como significa: faça uma ação
de E/S para cada elemento nesta lista. O que cada ação de E/S fará pode
depender do elemento usado para realizar a ação. Finalmente, execute
essas ações e vincule seus resultados a algo. Não precisamos vinculá-lo,
também podemos simplesmente jogá-lo fora.
$ runhaskell form_test.hs
Which color do you associate with the number 1?
white
Which color do you associate with the number 2?
blue
Which color do you associate with the number 3?
red
Which color do you associate with the number 4?
orange
The colors that you associate with 1, 2, 3 and 4 are:
white
blue
red
orange
Poderíamos realmente ter feito isso sem forM, apenas com
forM é mais legível. Normalmente, escrevemos
forM quando queremos mapear e sequenciar algumas ações que
definimos lá no local usando a notação do. Na mesma veia,
poderíamos ter substituído a última linha por
forM colors putStrLn.
Nesta seção, aprendemos o básico de entrada e saída. Também
descobrimos quais são as ações de E/S, como elas nos permitem fazer
entrada e saída e quando elas são realmente executadas. Para reiterar,
as ações de E/S são valores muito parecidos com qualquer outro valor em
Haskell. Podemos passá-las como parâmetros para funções e as funções
podem retornar ações de E/S como resultados. O que há de especial nelas
é que, se caírem na função main (ou são o resultado em uma
linha no GHCI), elas são executadas. E é aí que elas conseguem escrever
coisas na tela ou tocar Yakety Sax através de seus alto-falantes. Cada
ação de E/S também pode encapsular um resultado com o qual diz o que
obteve do mundo real.
Não pense em uma função como putStrLn como uma função
que pega uma string e a imprime na tela. Pense nisso como uma função que
pega uma string e retorna uma ação de E/S. Essa ação de E/S, quando
realizada, imprimirá belas poesias em seu terminal.
Arquivos e fluxos (Files and streams)

getChar é uma ação de E/S que lê um único caractere do
terminal. getLine é uma ação de E/S que lê uma linha do
terminal. Esses dois são bastante diretos e a maioria das linguagens de
programação tem algumas funções ou declarações paralelas a eles. Mas
agora, vamos conhecer getContents.
getContents é uma ação de E/S que lê tudo da entrada padrão
até encontrar um caractere de fim de arquivo. Seu tipo é
getContents :: IO String. O legal de
getContents é que ele faz a E/S preguiçosa (lazy I/O).
Quando fazemos foo <- getContents, ele não lê toda a
entrada de uma só vez, armazena na memória e depois liga para
foo. Não, é preguiçoso! Vai dizer: “Sim, sim, vou ler a
entrada do terminal mais tarde, à medida que avançamos, quando você
realmente presisar!”.
getContents é realmente útil quando estamos canalizando
(piping) a saída de um programa para a entrada de nosso programa. Caso
você não saiba como o encanamento (piping) funciona nos sistemas Unix-y,
aqui está uma introdução rápida. Vamos criar um arquivo de texto que
contenha o seguinte pequeno haiku:
I'm a lil' teapot
What's with that airplane food, huh?
It's so small, tasteless
Sim, o haiku é péssimo, e daí? Se alguém souber de bons tutoriais de haiku, me avise.
Agora, lembre-se do pequeno programa que escrevemos quando estávamos
apresentando a função forever. Ele pedia ao usuário uma
linha, devolvia a ele em CAPSLOCK e, em seguida, fazia tudo de novo,
indefinidamente. Apenas para que você não precise rolar todo o caminho
de volta, aqui está novamente:
import Control.Monad
import Data.Char
main = forever $ do
putStr "Give me some input: "
l <- getLine
putStrLn $ map toUpper l
Vamos salvar esse programa como capslocker.hs ou algo
assim e compilar. E então, vamos usar um pipe unix para alimentar nosso
arquivo de texto diretamente ao nosso pequeno programa. Usaremos a ajuda
do programa GNU cat, que imprime um arquivo que é dado a ele
como argumento. Confira, booyaka!
$ ghc --make capslocker
[1 of 1] Compiling Main ( capslocker.hs, capslocker.o )
Linking capslocker ...
$ cat haiku.txt
I'm a lil' teapot
What's with that airplane food, huh?
It's so small, tasteless
$ cat haiku.txt | ./capslocker
I'M A LIL' TEAPOT
WHAT'S WITH THAT AIRPLANE FOOD, HUH?
IT'S SO SMALL, TASTELESS
capslocker <stdin>: hGetLine: end of file
Como você pode ver, a canalização da saída de um programa (no nosso
caso era cat) para a entrada de outro (capslocker) é
feita com o caractere |. O que fizemos é praticamente
equivalente a apenas executar capslocker, digitar nosso haiku
no terminal e depois emitir um caractere de fim de arquivo (que
geralmente é feito pressionando Ctrl-D). É como executar cat
haiku.txt e dizer: “Espere, não imprima isso no terminal, diga para
o capslocker em vez disso!”.
Então, o que estamos fazendo essencialmente com esse uso de
forever está pegando a entrada e a transformando em alguma
saída. É por isso que podemos usar getContents para tornar
nosso programa ainda mais curto e melhor:
import Data.Char
main = do
contents <- getContents
putStr (map toUpper contents)
Executamos a ação de E/S getContents e nomeamos a string
que ela produz de contents. Então, mapeamos
toUpper sobre essa string e imprimimos isso no terminal.
Lembre-se de que, como strings são basicamente listas, que são
preguiçosas e getContents é I/O lazy, ele não tentará ler
todo o conteúdo de uma só vez e armazená-lo na memória antes de imprimir
a versão em caixa alta. Em vez disso, imprimirá a versão em caixa alta
enquanto a lê, porque apenas lerá uma linha da entrada quando realmente
precisar.
$ cat haiku.txt | ./capslocker
I'M A LIL' TEAPOT
WHAT'S WITH THAT AIRPLANE FOOD, HUH?
IT'S SO SMALL, TASTELESS
Legal, funciona. E se apenas executarmos capslocker e tentarmos digitar as linhas nós mesmos?
$ ./capslocker
hey ho
HEY HO
lets go
LETS GO
Saímos disso pressionando Ctrl-D. Muito legal! Como você pode ver,
imprimi nossa entrada em caixa alta de volta para nós, linha por linha.
Quando o resultado de getContents está vinculado a
contents, ele não é representado na memória como uma string
real, mas mais como uma promessa de que produzirá a string
eventualmente. Quando mapeamos toUpper sobre
contents, essa também é uma promessa de mapear essa função
sobre os conteúdos eventuais. E, finalmente, quando putStr
acontece, diz à promessa anterior: “Ei, preciso de uma linha em
caixa alta!”. Não tem nenhuma linha ainda, então diz a
contents: “Ei, que tal realmente obter uma linha do
terminal?”. Então é aí que getContents realmente lê do
terminal e fornece uma linha para o código que solicitou que ele
produzisse algo tangível. Esse código mapeia toUpper sobre
essa linha e o entrega a putStr, que a imprime. E então,
putStr diz: “Ei, preciso da próxima linha, vamos
lá!” e isso se repete até que não haja mais entrada, o que é
significado por um caractere de fim de arquivo.
Vamos criar programas que recebam algumas entradas e imprimam apenas as linhas que são menores que 10 caracteres. Observe:
main = do
contents <- getContents
putStr (shortLinesOnly contents)
shortLinesOnly :: String -> String
shortLinesOnly input =
let allLines = lines input
shortLines = filter (\line -> length line < 10) allLines
result = unlines shortLines
in result
Fizemos nossa parte de E/S do programa o mais curta possível. Como nosso programa deve obter alguma entrada e imprimir alguma saída com base na entrada, podemos implementá-lo lendo o conteúdo da entrada, executando uma função neles e depois imprimindo o que a função devolveu.
A função shortLinesOnly funciona assim: pega uma string,
como "short\nlooooooooooooooong\nshort again". Essa string
tem três linhas, duas delas são curtas e a do meio é longa. Ele executa
a função lines nessa string, que a converte em
["short", "looooooooooooooong", "short again"], à qual nos
ligamos ao nome allLines. Essa lista de strings é filtrada
para que apenas as linhas com menos de 10 caracteres permaneçam na
lista, produzindo ["short", "short again"]. E, finalmente,
unlines une essa lista em uma única string delimitada por
novas linhas (newlines), dando
"short\nshort again". Vamos tentar.
i'm short
so am i
i am a loooooooooong line!!!
yeah i'm long so what hahahaha!!!!!!
short line
loooooooooooooooooooooooooooong
short
$ ghc --make shortlinesonly
[1 of 1] Compiling Main ( shortlinesonly.hs, shortlinesonly.o )
Linking shortlinesonly ...
$ cat shortlines.txt | ./shortlinesonly
i'm short
so am i
short
Nós canalizamos (pipe) o conteúdo de shortlines.txt para shortlinesonly, e a saída contém apenas as linhas curtas.
Esse padrão de obter alguma string da entrada, transformá-la com uma
função e a saída é tão comum que existe uma função que facilita isso
ainda mais fácil, chamada interact.
interact leva uma função do tipo
String -> String como um parâmetro e retorna uma ação de
E/S que levará alguma entrada, executará essa função nela e depois
imprimirá o resultado da função. Vamos modificar nosso programa para
usar isso.
main = interact shortLinesOnly
shortLinesOnly :: String -> String
shortLinesOnly input =
let allLines = lines input
shortLines = filter (\line -> length line < 10) allLines
result = unlines shortLines
in result
Apenas para mostrar que isso pode ser alcançado com muito menos código (mesmo que seja menos legível) e demonstrar nossa habilidade de composição de funções, vamos reformular um pouco mais.
main = interact $ unlines . filter ((<10) . length) . lines
Uau, na verdade reduzimos isso a apenas uma linha, o que é muito legal!
interact pode ser usado para fazer programas em que são
canalizados (piped) alguns conteúdos e depois despejam algum resultado
ou pode ser usado para criar programas que parecem levar uma linha de
entrada do usuário, devolver algum resultado com base nessa linha e
depois pegar outra linha e assim por diante. Na verdade, não há uma
distinção real entre os dois, depende apenas de como o usuário deve
usá-los.
Vamos fazer um programa que leia continuamente uma linha e depois nos
diga se a linha é um palíndromo ou não. Poderíamos apenas usar
getLine para ler uma linha, dizer ao usuário se é um
palíndromo e depois executar main tudo de novo. Mas é mais
simples se usarmos interact. Ao usar interact,
pense no que você precisa fazer para transformar alguma entrada na saída
desejada. No nosso caso, temos que substituir cada linha da entrada por
"palindrome" ou "not a palindrome". Portanto,
temos que escrever uma função que transforme algo como
"elephant\nABCBA\nwhatever" em
"not a palindrome\npalindrome\nnot a palindrome". Vamos
fazer isso!
respondPalindromes contents = unlines (map (\xs -> if isPalindrome xs then "palindrome" else "not a palindrome") (lines contents))
where isPalindrome xs = xs == reverse xs
Vamos escrever isso em point-free.
respondPalindromes = unlines . map (\xs -> if isPalindrome xs then "palindrome" else "not a palindrome") . lines
where isPalindrome xs = xs == reverse xs
Bastante simples. Primeiro, transforma algo como
"elephant\nABCBA\nwhatever" em
["elephant", "ABCBA", "whatever"] e depois mapeia esse
lambda sobre ele, dando
["not a palindrome", "palindrome", "not a palindrome"] e
depois unlines une essa lista em uma única string
delimitada por novas linhas. Agora podemos fazer
main = interact respondPalindromes
Vamos testar isso:
$ runhaskell palindromes.hs
hehe
not a palindrome
ABCBA
palindrome
cookie
not a palindrome
Mesmo que tenhamos criado um programa que transforma uma grande série de entradas em outra, ele age como se tivéssemos criado um programa que o faz linha por linha. Isso ocorre porque Haskell é preguiçoso e quer imprimir a primeira linha da string de resultado, mas não pode porque ainda não tem a primeira linha da entrada. Assim que damos a primeira linha de entrada, ele imprime a primeira linha da saída. Saímos do programa emitindo um caractere de fim de linha.
Também podemos usar este programa apenas canalizando (piping) um arquivo para ele. Digamos que temos este arquivo:
dogaroo
radar
rotor
madam
e nós salvamos como words.txt. É isso que obtemos
canalizando-o para o nosso programa:
$ cat words.txt | runhaskell palindromes.hs
not a palindrome
palindrome
palindrome
palindrome
Novamente, obtemos a mesma saída como se tivéssemos executado o
programa e digitado as palavras nós mesmos na entrada padrão. A
diferença é que não vemos a entrada, porque foi fornecida a
palindromes.hs de um arquivo em vez de inserida diretamente
no teclado.
Então, agora você provavelmente vê como a E/S preguiçosa funciona e como podemos usá-la em nossa vantagem. Você pode pensar em termos de qual deve ser a saída para alguma entrada e escrever uma função para fazer essa transformação. Na E/S preguiçosa, nada é comido da entrada até que seja absolutamente necessário, porque o que queremos imprimir agora depende dessa entrada.
Até agora, trabalhamos com E/S imprimindo coisas no terminal e lendo
dele. Mas e a leitura e escrita de arquivos? Bem, de certa forma, já
estamos fazendo isso. Uma maneira de pensar sobre a leitura do terminal
é imaginar que é como ler de um arquivo (um pouco especial). O mesmo
vale para escrever no terminal, é como escrever em um arquivo. Podemos
chamar esses dois arquivos de stdout e stdin,
significando standard output (saída padrão) e standard
input (entrada padrão), respectivamente. Mantendo isso em mente,
veremos que escrever e ler arquivos é muito parecido com escrever na
saída padrão e na leitura da entrada padrão.
Começaremos com um programa realmente simples que abre um arquivo chamado girlfriend.txt, que contém um verso do hit número 1 de Avril Lavigne, Girlfriend, e apenas o imprime para o terminal. Aqui está girlfriend.txt:
Hey! Hey! You! You!
I don't like your girlfriend!
No way! No way!
I think you need a new one!
E aqui está o nosso programa:
import System.IO
main = do
handle <- openFile "girlfriend.txt" ReadMode
contents <- hGetContents handle
putStr contents
hClose handle
Executando-o, obtemos o resultado esperado:
$ runhaskell girlfriend.hs
Hey! Hey! You! You!
I don't like your girlfriend!
No way! No way!
I think you need a new one!
Vamos repassar isso linha por linha. A primeira linha são apenas quatro exclamações, para chamar nossa atenção. Na segunda linha, Avril nos diz que ela não gosta de nossa parceira romântica atual. A terceira linha serve para enfatizar essa desaprovação, enquanto a quarta linha sugere que devemos procurar uma nova namorada.
Vamos também passar pelo programa linha por linha! Nosso programa são
várias ações de E/S coladas com um bloco do. Na primeira linha
do bloco do, notamos uma nova função chamada openFile. Esta é a assinatura do seu tipo:
openFile :: FilePath -> IOMode -> IO Handle. Se você
ler em voz alta, ele afirma: openFile pega um caminho de
arquivo e um IOMode e retorna uma ação de E/S que abrirá um
arquivo e terá o identificador (handle) associado do arquivo
encapsulado como seu resultado.
FilePath é apenas um sinônimo
de tipo para String, simplesmente definido como:
type FilePath = String
IOMode é um tipo que é definido assim:
data IOMode = ReadMode | WriteMode | AppendMode | ReadWriteMode

Assim como nosso tipo que representa os sete valores possíveis para
os dias da semana, esse tipo é uma enumeração que representa o que
queremos fazer com nosso arquivo aberto. Muito simples. Observe apenas
que esse tipo é IOMode e não IO Mode.
IO Mode seria o tipo de ação de E/S que tem um valor de
algum tipo Mode como seu resultado, mas IOMode
é apenas uma enumeração simples.
Por fim, retorna uma ação de E/S que abrirá o arquivo especificado no
modo especificado. Se ligarmos essa ação a algo, obtemos um
Handle. Um valor do tipo Handle representa
onde está o nosso arquivo. Usaremos esse identificador para que saibamos
de qual arquivo ler. Seria estúpido ler um arquivo, mas não vincular
essa leitura a um identificador, porque não seríamos capazes de fazer
nada com o arquivo. Então, no nosso caso, vinculamos o identificador a
handle.
Na próxima linha, vemos uma função chamada hGetContents. É preciso um
Handle, de modo que sabe de qual arquivo obter o conteúdo e
retorna um IO String — uma ação de E/S que mantém como
resultado o conteúdo do arquivo. Esta função é muito parecida com
getContents. A única diferença é que
getContents lerá automaticamente a entrada padrão (ou seja,
do terminal), enquanto hGetContents recebe um identificador
de arquivos que diz a partir de qual arquivo ler. Em todos os outros
aspectos, elas funcionam da mesma maneira. E assim como
getContents, hGetContents não tentará ler o
arquivo de uma só vez e armazená-lo na memória, mas o lerá conforme
necessário. Isso é muito legal, porque podemos tratar
contents como todo o conteúdo do arquivo, mas ele não está
realmente carregado na memória. Portanto, se esse fosse um arquivo
realmente enorme, fazer hGetContents não engasgaria nossa
memória, mas leria apenas o que precisava do arquivo, quando
precisava.
Observe a diferença entre o identificador usado para identificar um
arquivo e o conteúdo do arquivo, vinculado ao nosso programa a
handle e contents. O identificador é apenas
algo pelo qual sabemos qual é o nosso arquivo. Se você imaginar todo o
seu sistema de arquivos como um livro realmente grande e cada arquivo é
um capítulo no livro, o identificador é um marcador que mostra onde você
está lendo (ou escrevendo) um capítulo, enquanto o conteúdo é o capítulo
real.
Com putStr contents, apenas imprimimos o conteúdo para a
saída padrão e depois fazemos hClose, que pega um identificador e
retorna uma ação de E/S que fecha o arquivo. Você tem que fechar o
arquivo depois de abri-lo com openFile!
Outra maneira de fazer o que acabamos de fazer é usar a função withFile, que tem uma assinatura de tipo
de
withFile :: FilePath -> IOMode -> (Handle -> IO a) -> IO a.
É preciso um caminho para um arquivo, um IOMode e depois é
necessária uma função que pegue um identificador e retorne alguma ação
de E/S. O que ele retorna é uma ação de E/S que abrirá esse arquivo,
fará algo que queremos com o arquivo e depois o fechará. O resultado
encapsulado na ação final de E/S que é retornada é o mesmo que o
resultado da ação de E/S que a função que damos a retorna. Isso pode
parecer um pouco complicado, mas é realmente simples, especialmente com
lambdas, aqui está o nosso exemplo anterior reescrito para usar
withFile:
import System.IO
main = do
withFile "girlfriend.txt" ReadMode (\handle -> do
contents <- hGetContents handle
putStr contents)
Como você pode ver, é muito semelhante ao pedaço de código anterior.
(\handle -> ... ) é a função que pega um identificador e
retorna uma ação de E/S e geralmente é feita assim, com um lambda. A
razão pela qual ele deve tomar uma função que retorna uma ação de E/S em
vez de apenas tomar uma ação de E/S para fazer e depois fechar o arquivo
é porque a ação de E/S que passamos para ele não saberia em qual arquivo
operar. Dessa forma, withFile abre o arquivo e passa o
identificador para a função que demos a ele. Ele recebe uma ação de E/S
de volta dessa função e, em seguida, faz uma ação de E/S exatamente
assim, apenas fecha o arquivo depois. Veja como podemos criar nossa
própria função withFile:
withFile' :: FilePath -> IOMode -> (Handle -> IO a) -> IO a
withFile' path mode f = do
handle <- openFile path mode
result <- f handle
hClose handle
return result

Sabemos que o resultado será uma ação de E/S, para que possamos
começar com um do. Primeiro abrimos o arquivo e obtemos um
identificador dele. Então, aplicamos handle à nossa função
para recuperar a ação de E/S que faz todo o trabalho. Vinculamos essa
ação a result, fechamos o manipulador e depois fazemos
return result. Ao retornar (return) o
resultado encapsulado na ação de E/S que obtivemos de f,
fazemos com que nossa ação de E/S encapsule o mesmo resultado que a que
recebemos de f handle. Portanto, se f handle
retornar uma ação que lerá várias linhas da entrada padrão e as
escreverá em um arquivo e terá como resultado encapsulado o número de
linhas que leu, se a usássemos com withFile', a ação de E/S
resultante também teria como resultado o número de linhas lidas.
Assim como temos hGetContents que funciona como
getContents, mas para um arquivo específico, também há
hGetLine, hPutStr, hPutStrLn, hGetChar, etc. Eles funcionam exatamente
como seus colegas sem o h, apenas eles usam um identificador
como um parâmetro e operam nesse arquivo específico, em vez de operar na
entrada padrão ou na saída padrão. Exemplo: putStrLn é uma
função que pega uma string e retorna uma ação de E/S que imprimirá essa
string no terminal e uma nova linha depois dela. hPutStrLn
pega um identificador e uma string e retorna uma ação de E/S que gravará
essa string no arquivo associado ao identificador e depois colocará uma
nova linha depois dela. Na mesma veia, hGetLine pega um
identificador e retorna uma ação de E/S que lê uma linha de seu
arquivo.
Carregar arquivos e tratar seu conteúdo como strings é tão comum que temos estas três pequenas funções legais para facilitar ainda mais nosso trabalho:
readFile tem uma assinatura de
tipo de readFile :: FilePath -> IO String. Lembre-se,
FilePath é apenas um nome chique para String.
readFile pega um caminho para um arquivo e retorna uma ação
de E/S que lerá esse arquivo (preguiçosamente, é claro) e vinculará seu
conteúdo a algo como uma string. Geralmente é mais útil do que fazer
openFile e vinculá-lo a um identificador e depois fazer
hGetContents. Veja como poderíamos ter escrito nosso
exemplo anterior com readFile:
import System.IO
main = do
contents <- readFile "girlfriend.txt"
putStr contents
Como não obtemos um identificador com o qual identificar nosso
arquivo, não podemos fechá-lo manualmente, então Haskell faz isso por
nós quando usamos readFile.
writeFile tem um tipo de
writeFile :: FilePath -> String -> IO (). É preciso
um caminho para um arquivo e uma string para gravar nesse arquivo e
retorna uma ação de E/S que fará a escrita. Se esse arquivo já existir,
ele será reduzido para o comprimento zero antes de ser escrito. Veja
como transformar girlfriend.txt em uma versão em CAPSLOCK e
escrevê-la para girlfriendcaps.txt:
import System.IO
import Data.Char
main = do
contents <- readFile "girlfriend.txt"
writeFile "girlfriendcaps.txt" (map toUpper contents)
$ runhaskell girlfriendtocaps.hs
$ cat girlfriendcaps.txt
HEY! HEY! YOU! YOU!
I DON'T LIKE YOUR GIRLFRIEND!
NO WAY! NO WAY!
I THINK YOU NEED A NEW ONE!
appendFile tem uma assinatura de
tipo exatamente como writeFile, apenas
appendFile não trunca o arquivo para o comprimento zero se
ele já existir, mas anexa coisas a ele.
Digamos que temos um arquivo todo.txt que tem uma tarefa por linha que temos que fazer. Agora, vamos fazer um programa que adote uma linha da entrada padrão e a adicione à nossa lista de tarefas.
import System.IO
main = do
todoItem <- getLine
appendFile "todo.txt" (todoItem ++ "\n")
$ runhaskell appendtodo.hs
Iron the dishes
$ runhaskell appendtodo.hs
Dust the dog
$ runhaskell appendtodo.hs
Take salad out of the oven
$ cat todo.txt
Iron the dishes
Dust the dog
Take salad out of the oven
Precisávamos adicionar o "\n" ao final de cada linha,
porque getLine não nos dá um caractere de nova linha no
final.
Ooh, mais uma coisa. Conversamos sobre como fazer
contents <- hGetContents handle não faz com que todo o
arquivo seja lido de uma só vez e armazenado na memória. É I/O lazy,
então fazer isso:
main = do
withFile "something.txt" ReadMode (\handle -> do
contents <- hGetContents handle
putStr contents)
é realmente como conectar um tubo (pipe) do arquivo à saída. Assim como você pode pensar em listas como fluxos (streams), também pode pensar em arquivos como fluxos. Isso lerá uma linha de cada vez e a imprimirá no terminal à medida que avança. Então você pode estar perguntando, qual a largura desse tubo então? Com que frequência o disco será acessado? Bem, para arquivos de texto, o buffer padrão é geralmente o buffer de linha (line-buffering). Isso significa que a menor parte do arquivo a ser lida de uma vez é uma linha. É por isso que, nesse caso, ele realmente lê uma linha, a imprime na saída, lê a próxima linha, a imprime, etc. Para arquivos binários, o buffer padrão geralmente é o buffer de bloco (block-buffering). Isso significa que ele lerá o arquivo pedaço por pedaço. O tamanho do pedaço é de algum tamanho que seu sistema operacional acha legal.
Você pode controlar exatamente como o buffer é feito usando a função
hSetBuffering. É preciso um identificador e um
BufferMode e retorna uma ação de E/S que define o buffer.
BufferMode é um tipo de dados de enumeração simples e os
valores possíveis que ele pode conter são: NoBuffering,
LineBuffering ou BlockBuffering (Maybe Int). O
Maybe Int é para o tamanho do pedaço, em bytes. Se for
Nothing, o sistema operacional determina o tamanho do
pedaço. NoBuffering significa que será lido um caractere de
cada vez. NoBuffering geralmente é uma merda como um modo
de buffer, porque tem que acessar muito o disco.
Aqui está o nosso pedaço de código anterior, só que não o lê linha por linha, mas lê todo o arquivo em pedaços de 2048 bytes.
main = do
withFile "something.txt" ReadMode (\handle -> do
hSetBuffering handle $ BlockBuffering (Just 2048)
contents <- hGetContents handle
putStr contents)
A leitura de arquivos em pedaços maiores pode ajudar se quisermos minimizar o acesso ao disco ou quando nosso arquivo for realmente um recurso de rede lento.
Também podemos usar hFlush, que é
uma função que usa um identificador e retorna uma ação de E/S que
liberará o buffer do arquivo associado ao identificador. Quando estamos
fazendo o buffer de linha, o buffer é liberado (flushed) após cada
linha. Quando estamos fazendo buffer de bloco, é depois de lermos um
pedaço. Também é liberado depois de fechar um identificador. Isso
significa que quando chegamos a um caractere de nova linha, o mecanismo
de leitura (ou gravação) relata todos os dados até agora. Mas podemos
usar hFlush para forçar esse relatório de dados que foram
lidos até agora. Após a liberação, os dados estão disponíveis para
outros programas que estão sendo executados ao mesmo tempo.
Pense em ler um arquivo em buffer de bloco como este: o vaso
sanitário está programado para se liberar (dar descarga) depois de ter
um galão de água dentro dele. Então você começa a derramar água e, uma
vez atingida a marca de galão, essa água é lavada automaticamente e os
dados na água que você derramou até agora são lidos. Mas você também
pode lavar o banheiro manualmente pressionando o botão no banheiro. Isso
faz com que o banheiro dê descarga e toda a água (dados) dentro do
banheiro seja lida. Caso você não tenha notado, liberar (dar descarga
d’) o banheiro manualmente é uma metáfora para hFlush. Essa
não é uma analogia muito boa para os padrões de analogia de programação,
mas eu queria um objeto do mundo real que possa ser liberado (flushed)
para a piada (punchline).
Já fizemos um programa para adicionar um novo item à nossa lista de
tarefas em todo.txt, agora vamos fazer um programa para remover
um item. Apenas colarei o código e depois passaremos pelo programa
juntos para que você veja que é realmente fácil. Usaremos algumas novas
funções de System.Directory e uma nova função de
System.IO, mas todas serão explicadas.
De qualquer forma, aqui está o programa para remover um item de todo.txt:
import System.IO
import System.Directory
import Data.List
main = do
handle <- openFile "todo.txt" ReadMode
(tempName, tempHandle) <- openTempFile "." "temp"
contents <- hGetContents handle
let todoTasks = lines contents
numberedTasks = zipWith (\n line -> show n ++ " - " ++ line) [0..] todoTasks
putStrLn "These are your TO-DO items:"
putStr $ unlines numberedTasks
putStrLn "Which one do you want to delete?"
numberString <- getLine
let number = read numberString
newTodoItems = delete (todoTasks !! number) todoTasks
hPutStr tempHandle $ unlines newTodoItems
hClose handle
hClose tempHandle
removeFile "todo.txt"
renameFile tempName "todo.txt"
No início, abrimos todo.txt no modo de leitura e vinculamos
seu identificador a handle.
Em seguida, usamos uma função que não encontramos antes, que é de
System.IO — openTempFile. Seu nome é bastante
auto-explicativo. É preciso um caminho para um diretório temporário e um
nome de modelo (template) para um arquivo e abre um arquivo temporário.
Usamos "." para o diretório temporário, porque
. denota o diretório atual em praticamente qualquer sistema
operacional. Usamos "temp" como o nome do modelo para o
arquivo temporário, o que significa que o arquivo temporário será
nomeado temp mais alguns caracteres aleatórios. Ele retorna uma
ação de E/S que cria o arquivo temporário e o resultado nessa ação de
E/S é um par de valores: o nome do arquivo temporário e um
identificador. Poderíamos apenas abrir um arquivo normal chamado
todo2.txt ou algo assim, mas é melhor praticar usar
openTempFile para que você saiba que provavelmente não está
sobrescrevendo nada.
A razão pela qual não usamos getCurrentDirectory para
obter o diretório atual e depois passá-lo para
openTempFile, mas apenas passamos "." para
openTempFile é porque . refere-se ao diretório
atual no sistema semelhante ao Unix e Windows
Em seguida, vinculamos o conteúdo de todo.txt a
contents. Em seguida, divida essa string em uma lista de
strings, cada string uma linha. Portanto, todoTasks agora é
algo como
["Iron the dishes", "Dust the dog", "Take salad out of the oven"].
Nós compactamos (zip) os números de 0 em diante e essa lista com uma
função que pega um número, como 3, e uma string, como "hey"
e retorna "3 - hey", então numberedTasks é
["0 - Iron the dishes", "1 - Dust the dog" .... Juntamos
essa lista de strings em uma única string delimitada por novas linhas
com unlines e imprimimos essa string no terminal. Observe
que, em vez de fazer isso, também poderíamos ter feito
mapM putStrLn numberedTasks
Perguntamos ao usuário qual deles eles querem excluir e esperar que
eles insira um número. Digamos que eles desejam excluir o número 1, que
é Dust the dog, então eles digitam 1.
numberString agora é "1" e porque queremos um
número, não uma string, corremos read para obter
1 e vinculamos isso a number.
Lembre-se das funções delete e !! de
Data.List. !! retorna um elemento de uma lista
com algum índice e delete exclui a primeira ocorrência de
um elemento em uma lista e retorna uma nova lista sem essa ocorrência.
(todoTasks !! number) (number agora é 1)
retorna "Dust the dog". Vinculamos todoTasks
sem a primeira ocorrência de "Dust the dog" a
newTodoItems e depois juntamos tudo em uma única string com
unlines antes de grava-lá no arquivo temporário que
abrimos. O arquivo antigo agora está inalterado e o arquivo temporário
contém todas as linhas que o antigo, exceto a que excluímos.
Depois disso, fechamos os arquivos originais e temporários e depois
removemos o original com removeFile,
que, como você pode ver, pega um caminho para um arquivo e o exclui.
Depois de excluir o antigo todo.txt, usamos renameFile para renomear o arquivo
temporário para todo.txt. Tenha cuidado,
removeFile e renameFile (que estão ambos em
System.Directory a propósito) usam caminhos de arquivo como
seus parâmetros, não identificadores.
E é isso! Poderíamos ter feito isso em ainda menos linhas, mas tomamos muito cuidado para não substituir nenhum arquivo existente e educadamente pedimos ao sistema operacional que nos dissesse onde podemos colocar nosso arquivo temporário. Vamos tentar!
$ runhaskell deletetodo.hs
These are your TO-DO items:
0 - Iron the dishes
1 - Dust the dog
2 - Take salad out of the oven
Which one do you want to delete?
1
$ cat todo.txt
Iron the dishes
Take salad out of the oven
$ runhaskell deletetodo.hs
These are your TO-DO items:
0 - Iron the dishes
1 - Take salad out of the oven
Which one do you want to delete?
0
$ cat todo.txt
Take salad out of the oven
Argumentos de linha de comando (Command line arguments)

Se você deseja executar um script ou aplicativo de um terminal,
geralmente é um pouco desajeitado percorrer as etapas de executar o
programa, navegando um pouco nos menus (ou digitando em um prompt) e
depois saindo. Em vez disso, o que você quer fazer é alimentar os
parâmetros do aplicativo ao executá-lo, para que ele já saiba o que deve
fazer quando for lançado. Se você é o tipo de pessoa que evita a dor de
usar o Windows batendo a cabeça no terminal linux, você sabe do que
estou falando. Diferentemente de apenas mv, que renomearia
o arquivo atual ou o mudaria para outro diretório, fazemos
mv file source destination e todo o trabalho é tratado
naquele instante. file, source e
destination são chamados de argumentos de linha de comando.
A função CommandLineArgument permite acessar esses
argumentos de linha de comando.
A função System.Environment possui duas funções I/O
legais e interessantes. Uma é getArgs, que tem um tipo de
getArgs :: IO [String] e é uma ação de E/S que obterá os
argumentos que o programa foi executado e os terá como resultado
contido. A outra função é getProgName, que tem um tipo de
getProgName :: IO String e é uma ação de E/S que contém o
nome do programa.
Aqui está um pequeno programa que demonstra como isso funciona:
import System.Environment
import Data.List
main = do
args <- getArgs
progName <- getProgName
putStrLn "The arguments are:"
mapM putStrLn args
putStrLn "The program name is:"
putStrLn progName
Nós vinculamos getArgs a args e
getProgName a progName. args é
apenas uma lista de strings, nas quais podemos usar mapear as funções de
processamento de listas, mapM e o que mais você quiser.
Aqui nós apenas imprimimos os argumentos um por um e também o nome do
programa. Vamos compilar isso como arg-test:
$ ghc --make arg-test
[1 of 1] Compiling Main ( arg-test.hs, arg-test.o )
Linking arg-test ...
$ ./arg-test first second w00t "multi word arg"
The arguments are:
first
second
w00t
multi word arg
The program name is:
arg-test
Legal. Armados com esse conhecimento, poderíamos criar alguns
aplicativos de linha de comando legais. No momento, vamos fazer um
programa que adote dois argumentos da linha de comando. O primeiro
argumento será o nome do arquivo da nossa lista de tarefas, e o segundo
será uma ação que queremos realizar nesse arquivo, o que pode ser:
visualizar (view), adicionar (add) ou remover
(remove). Se a ação for add, o programa
aceitará outro argumento, que é a tarefa a adicionar a nossa lista. Se a
ação for remove, ela pegará outro argumento, que é o índice
da entrada a remover.
A coisa legal sobre Haskell é que, se você deseja criar um programa que faça uma tarefa específica, digamos, processando a entrada, geralmente pode começar escrevendo uma função que faz exatamente isso, mas tudo em código puro. Somente depois disso você escreve a parte suja de E/S que lê a entrada, chama sua função nos dados e imprime o resultado. Mas aqui, como estamos lidando com a linha de comando, não podemos ter uma função pura agradável processando a entrada, porque o que fazemos depende dos argumentos de linha de comando. No entanto, podemos criar algumas funções para lidar com as ações antes mesmo de fazermos a parte que processa a linha de comando.
Começaremos fazendo algumas importações.
import System.Environment
import System.Directory
import System.IO
import Data.List
Agora, vamos fazer as funções que queremos ter. Todos elas seguirão o
trabalho com nossa lista de tarefas: visualizar itens, adicionar itens e
excluir itens. A função view fará a visualização.
view :: [String] -> IO ()
view [fileName] = do
contents <- readFile fileName
let todoTasks = lines contents
numberedTasks = zipWith (\n line -> show n ++ " - " ++ line) [0..] todoTasks
putStr $ unlines numberedTasks
Se tentarmos avaliar isso no GHCI sem os argumentos, dirá que falhou
a correspondência de padrões na função view. Isso ocorre
porque a função espera uma lista que contenha exatamente um elemento, o
nome do arquivo. No nosso programa, leremos argumentos na linha de
comando e os aplicaremos a essa função, por isso, garantiremos que essa
lista de argumentos tenha apenas um elemento. O que o view
faz é bastante simples. Ele lê o arquivo, imprime suas linhas (depois de
numerá-las apropriadamente) e pronto.
Agora, para a função add adicionar tarefas:
add :: [String] -> IO ()
add [fileName, todoItem] = appendFile fileName (todoItem ++ "\n")
Também aceita uma lista de argumentos, que esperamos que tenham
exatamente dois elementos: um que seja o nome do arquivo no qual
queremos adicionar a tarefa e a que é a própria tarefa. Ele usa
appendFile para empurrar essa linha até o final do
arquivo.
Por fim, remover:
remove :: [String] -> IO ()
remove [fileName, numberString] = do
handle <- openFile fileName ReadMode
(tempName, tempHandle) <- openTempFile "." "temp"
contents <- hGetContents handle
let number = read numberString
todoTasks = lines contents
newTodoItems = delete (todoTasks !! number) todoTasks
hPutStr tempHandle $ unlines newTodoItems
hClose handle
hClose tempHandle
removeFile fileName
renameFile tempName fileName
Isso é basicamente o mesmo programa que usamos na remoção anteriormente, apenas encapsulado em uma função. Recebe uma lista de argumentos e espera que ela contenha o nome do arquivo para excluir um item e a string que representa o índice do item.
Então, definimos nossas três funções. Agora, vamos fazer uma lista de associação que mapeie argumentos de linha de comando para essas funções.
dispatch :: [(String, [String] -> IO ())]
dispatch = [ ("add", add)
, ("view", view)
, ("remove", remove)
]
Isso é tudo, apenas uma lista simples de associações. O tipo da lista
e seus valores podem ser um pouco difíceis a princípio, mas uma vez que
você pensa sobre a definição, ela faz sentido. Lembre-se,
add, view e remove têm um tipo de
[String] -> IO (), portanto, os elementos da lista
dispatch são pares (tuplas de 2 elementos): o primeiro
elemento é uma string (nossos comandos) e o segundo elemento é uma
função.
Por fim, a função principal do nosso aplicativo:
main = do
(command:args) <- getArgs
let (Just action) = lookup command dispatch
action args
A primeira coisa que fazemos é obter os argumentos e vinculá-los a
(command:args). Se chamarmos nosso aplicativo como
./todo view todo.txt, command será
"view" e args será ["todo.txt"].
A próxima linha procura nosso comando na lista dispatch.
Como "view" aponta para view, temos
Just view. Se usarmos correspondência de padrões para
extrair a ação de Just, ela retornará view.
Finalmente, chamamos o action args, que é o mesmo que
chamar view args, que é o mesmo que chamar
view ["todo.txt"]. Massa!
Se você executar o todo, mas não fornecer nenhum
argumento, o tempo de execução gritará com você. Isso porque, quando
correspondemos ao padrão (command:args) com o resultado de
getArgs, ele espera uma lista com pelo menos um elemento.
Se essa lista estiver vazia, o padrão falhará.
Vamos ver se funciona.
$ ghc --make todo
[1 of 1] Compiling Main ( todo.hs, todo.o )
Linking todo ...
$ ./todo view todo.txt
Iron the dishes
Dust the dog
Take salad out of the oven
$ ./todo add todo.txt "Pick up children from drycleaners"
$ ./todo view todo.txt
Iron the dishes
Dust the dog
Take salad out of the oven
Pick up children from drycleaners
$ ./todo remove todo.txt 2
$ ./todo view todo.txt
Iron the dishes
Dust the dog
Pick up children from drycleaners
Tudo parece estar funcionando! Note que nós não validamos os
argumentos do usuário, por exemplo. Se alguém executar
./todo blorg 1, nosso programa falhará, porque
blorg não está na lista dispatch. Mas ei, esta
é apenas uma demonstração, não um software de nível empresarial que pode
falhar quando você tenta se dividir por zero (apenas brincando, os
programas Haskell nunca falham dessa maneira!).
Aleatoriedade (Randomness)

Muitas vezes, enquanto programa, você precisa obter alguns dados
aleatórios. Talvez você esteja construindo um jogo em que os dados
precisem ser lançados ou precise gerar alguns dados de teste para testar
seu programa. Existem várias maneiras de fazer com que o computador gere
números aleatórios para nós, mas a maioria deles depende de nós obter
algumas sementes (random seeds) físicas e de sistema. Se definirmos a
semente desse gerador aleatório para a mesma coisa, nosso programa
poderá gerar os mesmos “números aleatórios” toda vez que for executado.
Em outras linguagens, as funções que nos dão certa aleatoriedade mudam
algum tipo de estado oculto (como uma semente aleatória global), assim
como faz E/S e, portanto, porque é impuro e não pode ser feito no código
funcional puro. Haskell tem uma função que retorna um número aleatório?
Bem, pense sobre isso. Se tivermos um gerador de números aleatórios como
uma função, ele teria que ter um tipo de
foo :: () -> Int (ou algum outro tipo de número). Mas
isso significa que ele sempre retornaria o mesmo número e não seria
aleatório. Portanto, para obter um número aleatório, temos a ideia de
obter um objeto que representa um gerador de números aleatórios e passar
isso para alguma função e obter de volta um número e um novo gerador de
números aleatórios que usaremos para obter o próximo número e o próximo
gerador e assim por diante. Isso significa que, se usarmos duas vezes a
mesma função com o mesmo gerador, obteremos o mesmo resultado. E isso é
incrível porque significa que nosso código pode ser puro, mesmo que
geremos números aleatórios! Para a mesma semente, sempre obteremos a
mesma “aleatoriedade”. Se quisermos que dois números diferentes sejam
gerados, basta obter o primeiro número e o novo gerador e passaremos o
novo gerador para a função novamente. Isso também significa que podemos
reproduzir situações aleatórias em nosso programa (como reproduzir uma
falha que apenas acontece em determinadas situações com números
aleatórios específicos), o que é impossível em outras linguagens, porque
não há garantia de que você obtenha os mesmos números aleatórios duas
vezes consecutivas.
O módulo System.Random tem todas as funções que
satisfazem nossa necessidade de aleatoriedade. Vamos mergulhar em uma
função que é a chave para gerar números aleatórios. Chame a polícia, é
random! Seu tipo é
random :: (RandomGen g, Random a) => g -> (a, g).
Uau, algumas novas typeclasses nesta declaração de tipo! A classe de
tipo RandomGen é para tipos que podem atuar como geradores
de aleatoriedade. A classe de tipo Random é para coisas que
podem assumir valores aleatórios. Valores booleanos, caracteres,
inteiros, duplos, etc. Se tentarmos traduzir a declaração de tipo para o
inglês, seria algo como: pega um gerador aleatório (que é uma instância
de RandomGen) e retorna um valor aleatório e um novo
gerador aleatório. Por que ele retorna um novo gerador aleatório, além
do valor aleatório? Bem, como dissemos anteriormente, o gerador de
números aleatórios representa o estado da computação de números
aleatórios. Se apenas tivéssemos uma função que gerasse um número
aleatório, teria que alterar o único gerador de números aleatórios
existente e, portanto, teria que ser uma função impura, porque Haskell é
puro. A função random usa um gerador existente e retorna um
novo gerador estado.
O tipo padrão que é instância de RandomGen do
System.Random é o StdGen. Para fazer
manualmente um StdGen, podemos usar a função mkStdGen, que tem um tipo de
mkStdGen :: Int -> StdGen. Pega um número inteiro e com
base nisso, nos dá um gerador aleatório. Ok, vamos tentar usar
random e mkStdGen em conjunto para obter um
número aleatório.
ghci> random (mkStdGen 100)

O que é isso? Ah, certo, a função random pode retornar
um valor de qualquer tipo que faz parte da classe de tipos
Random, então temos que informar a Haskell que tipo de tipo
queremos. Não assumirá que queremos um Int ou um
Double. Lembre-se também de que ele retorna um par que
consiste no valor aleatório e em outro gerador.
ghci> random (mkStdGen 100) :: (Int, StdGen)
(-1352021624,651872571 1655838864)
ghci> random (mkStdGen 100) :: (Int, StdGen)
(-1352021624,651872571 1655838864)
Finalmente! Um número que parece meio aleatório! Como você pode ver,
a representação de texto StdGen não consegue ensinar muito
sobre o que está por trás disso. Mas se chamarmos a função duas vezes
com os mesmos parâmetros (o mesmo gerador), obtemos o mesmo resultado,
conforme esperado. Agora vamos tentar simular um arremesso de uma moeda
com nossos dados aleatórios.
ghci> random (mkStdGen 100) :: (Int, StdGen)
(-1352021624,651872571 1655838864)
ghci> random (mkStdGen 100) :: (Bool, StdGen)
(False,4041414 40692)
ghci> random (mkStdGen 100) :: (Float, StdGen)
(0.7915302,159796065 40692)
ghci> random (mkStdGen 949494) :: (Int, StdGen)
(539963926,466647808 1655838864)
ghci> random (mkStdGen 949488) :: (Float, StdGen)
(0.8938442,1597348957 1655838864)
ghci> random (mkStdGen 949488) :: (Bool, StdGen)
(False,1485632275 40692)
Por que random não sabe que deve retornar um booleano
quando dizemos que queremos um arremesso de moeda? Basicamente, porque
não sabe que chamamos isso de “moeda”. Uma moeda tem dois valores,
heads (cara) e tails (coroa), que mapeia bem
para Bool. E já temos Bools como instâncias de
Random.
Vamos fazer uma função que simula atirar uma moeda três vezes.
threeCoins :: StdGen -> (Bool, Bool, Bool)
threeCoins gen =
let (firstCoin, newGen) = random gen
(secondCoin, newGen') = random newGen
(thirdCoin, newGen'') = random newGen'
in (firstCoin, secondCoin, thirdCoin)
Nós chamamos de random com o gerador que recebemos como
parâmetro para obter uma moeda e um novo gerador. Então nós chamamos
novamente, apenas com o novo gerador, para obter a segunda moeda.
Fazemos a mesma coisa com a terceira moeda. Se tivéssemos chamado de
“random” com o mesmo gerador todas as 3 vezes, todas as 3 moedas teriam
o mesmo valor e seriam todas iguais.
ghci> threeCoins (mkStdGen 21)
(True,True,True)
ghci> threeCoins (mkStdGen 22)
(True,False,True)
ghci> threeCoins (mkStdGen 943)
(True,False,True)
ghci> threeCoins (mkStdGen 944)
(True,True,True)
E se quisermos virar 4 moedas? Ou 5? Bem, existe uma função chamada
randoms que assume um gerador e
retorna uma lista infinita de valores com base nesse gerador.
ghci> take 5 $ randoms (mkStdGen 11) :: [Int]
[-1807975507,545092095,-1015194702,-1622477312,-502893664]
ghci> take 5 $ randoms (mkStdGen 11) :: [Bool]
[True,True,True,True,False]
ghci> take 5 $ randoms (mkStdGen 11) :: [Float]
[7.904792e-2,0.62566846,0.25612664,0.46784747,0.3687355]
Por que randoms não retorna um novo gerador, além de uma
lista? Poderíamos implementar a função randoms com muita
facilidade, assim:
randoms' :: (RandomGen g, Random a) => g -> [a]
randoms' gen = let (value, newGen) = random gen in value : randoms' newGen
Uma definição recursiva. Obtemos um valor aleatório e um novo gerador do gerador atual e, em seguida, fazemos uma lista que tem o valor como cabeça e números aleatórios gerados com o novo gerador como sua cauda. Como temos que ser capazes de gerar uma quantidade infinita de números, não podemos devolver o novo gerador.
Também há a função randomR, que
tem o tipo
randomR :: (RandomGen g, Random a) => (a, a) -> g -> (a, g).
É como random, mas leva como seu primeiro parâmetro um par
de valores que estabelecem limites inferior e superior e o valor final
produzido cai dentro desses limites.
ghci> randomR (1,6) (mkStdGen 359353)
(6,1494289578 40692)
ghci> randomR (1,6) (mkStdGen 35935335)
(1,1250031057 40692)
Há também randomRs, que produz um fluxo de valores
aleatórios dentro de um intervalo. Confira isso:
ghci> take 10 $ randomRs ('a','z') (mkStdGen 3) :: [Char]
"ndkxbvmomg"
Legal, parece uma senha super secreta ou algo assim.
Então você pode estar se perguntando: o que isso tem a ver com I/O?
Nós não produzimos nada até agora. Bem, até agora, sempre criamos nosso
gerador de números aleatórios manualmente com alguma semente (semente
nada mais é do que um inteiro). O problema é que, se fizermos isso em
nossos programas reais, eles sempre devolverão os mesmos números
aleatórios, o que não é bom para nós. É por isso que
System.Random oferece a ação de E/S getStdGen, que tem um tipo de
getStdGen :: IO StdGen. Quando seu programa começa, ele
pede ao sistema um bom gerador de números aleatórios e o armazena no
chamado gerador global. getStdGen busca esse gerador global
para você vinculá-lo a alguma coisa.
Aqui está um programa simples que gera uma string aleatória.
import System.Random
main = do
gen <- getStdGen
putStr $ take 20 (randomRs ('a','z') gen)
$ runhaskell random_string.hs
pybphhzzhuepknbykxoo
$ runhaskell random_string.hs
pybphhzzhuepknbykxoo
Epa! Aqui na segunda execução imprimiu a mesma coisa. Isso ocorre
porque fizemos getStdGen duas vezes. Na primeira vez, obtém
o gerador global, na segunda vez recebe o gerador global. O mesmo
gerador, portanto, a mesma saída. Para obter dois resultados diferentes,
teríamos que atualizar o gerador global. Fazemos isso, chocantemente,
usando a função newStdGen, que
divide nosso gerador atual em dois geradores novos (splitting).
Ele atualiza o gerador global com um deles e encapsula o outro como
resultado.
import System.Random
main = do
gen <- getStdGen
putStrLn $ take 20 (randomRs ('a','z') gen)
gen2 <- newStdGen
putStr $ take 20 (randomRs ('a','z') gen2)
$ runhaskell random_string.hs
zjy svdvnlllguykhsae
ncsvkphonxryktfhbmct
Não apenas obtemos uma nova string aleatória quando vinculamos
newStdGen a gen2, mas o gerador global é
atualizado; portanto, se fizermos getStdGen novamente e a
vincularmos a algo, teremos um gerador que não é o mesmo que
gen.
Aqui está um pequeno programa que fará com que o usuário adivinhe qual é o número que está pensando.
import System.Random
import Control.Monad(when)
main = do
gen <- getStdGen
askForNumber gen
askForNumber :: StdGen -> IO ()
askForNumber gen = do
let (randomNumber, newGen) = randomR (1,10) gen :: (Int, StdGen)
putStr "Which number in the range from 1 to 10 am I thinking of? "
numberString <- getLine
when (not $ null numberString) $ do
let number = read numberString
if randomNumber == number
then putStrLn "You are correct!"
else putStrLn $ "Sorry, it was " ++ show randomNumber
askForNumber newGen
Nós criamos uma função askForNumber, que recebe um
gerador e retorna uma ação de E/S que solicitará um número ao usuário e
dirá se ele adivinhou certo. Nessa função, primeiro geramos um número
aleatório e um novo gerador com base no gerador que obtivemos como
parâmetro e os chamamos de randomNumber e
newGen. Dizemos que é um Int, porque
read não saberia de outra forma para o que ler. Em seguida,
obtemos a entrada do usuário e a transformamos em um número. Verificamos
se o número que eles inseriram é igual ao número gerado aleatoriamente e
damos a mensagem apropriada. E então chamamos askForNumber
recursivamente, apenas desta vez com o novo gerador que recebemos, o que
nos dá um novo número. No main, apenas obtemos um gerador
padrão e chamamos askForNumber. Observe que apenas
verificamos se a entrada do usuário é nula (null) ou
não.
$ runhaskell guess_the_number.hs
Which number in the range from 1 to 10 am I thinking of? 4
Sorry, it was 3
Which number in the range from 1 to 10 am I thinking of? 10
You are correct!
Which number in the range from 1 to 10 am I thinking of? 2
Sorry, it was 4
Which number in the range from 1 to 10 am I thinking of?
Outra maneira de fazer o mesmo é assim:
import System.Random
import Control.Monad(when)
main = do
gen <- getStdGen
let (randomNumber, _) = randomR (1,10) gen :: (Int, StdGen)
putStr "Which number in the range from 1 to 10 am I thinking of? "
numberString <- getLine
when (not $ null numberString) $ do
let number = read numberString
if randomNumber == number
then putStrLn "You are correct!"
else putStrLn $ "Sorry, it was " ++ show randomNumber
newStdGen
main
É muito semelhante ao anterior, apenas em vez de fazer uma função que
pega um gerador e depois se chama recursivamente com o novo gerador,
fazemos todo o trabalho no main. Depois de informar ao
usuário se eles estão corretos ou não, atualizamos o gerador global e
depois chamamos o main novamente. Tanto a abordagem
anterior quanto essa estão bem.
newStdGen é legal porque não precisamos passar um
gerador para as funções ou fazer as coisas no main apenas
para garantir que obteremos um novo número aleatório na próxima vez.
Bytestrings
Listas são uma estrutura de dados legal e útil. A maioria que
encontramos até agora é uma lista. Haskell Strings são
apenas listas de caracteres. Seus tipos de declarações são as mesmas e
também as funções para processamento. Então, String é
apenas uma lista preguiçosa. Isso é útil porque podemos interagir com
essas strings por quanto tempo quisermos sem carregá-las de uma vez na
memória… Mas também há uma sobrecarga associada a isso. Isso porque um
Int em uma lista não é apenas um número, mas, na verdade,
uma promessa (thunk) para um número (porque Haskell é
preguiçoso). Essa promessa pode ser avaliada para o número, apenas não
sabemos quando.
Muitas vezes, essa sobrecarga não nos incomoda, mas acontece quando lemos arquivos grandes ou manipulamos grandes strings, isso nos deixa mais lentos. É aí que os Bytestrings entram. Bytestrings são parecidos com uma lista, mas cada elemento é um byte (ou um número de 8 bits), e a maneira como eles lidam com a preguiça é diferente.
Bytestrings vêm em dois sabores: estritos (strict) e preguiçosos
(lazy). Bytestrings estritos residem em Data.ByteString e
são uma série de bytes em uma matriz. Você não pode avaliá-los
preguiçosamente. Não haveria thunks (promessas de computação). A
vantagem é que há muito menos sobrecarga; desvantagem é que eles
preenchem sua memória mais rapidamente.
O outro tipo de bytestrings reside em
Data.ByteString.Lazy. Eles são preguiçosos, mas não tão
preguiçosos quanto as listas. Em uma lista, não há tanta diferença entre
ter 1 elemento ou 200 elementos: eles são apenas promessas. Uma
bytestring preguiçosa é armazenada em pedaços (64K), onde cada pedaço
tem um tamanho fixo. Bytestrings preguiçosos são listas de pedaços de
64K. Quando você avalia um byte em uma string preguiçosa (pelo
L.head, por exemplo), o primeiro pedaço é avaliado. Depois
disso, é apenas uma lista de pedaços! Portanto, a estrutura de dados é
um pouco diferente da das listas, mas não muito. Isso permite processar
um arquivo muito rapidamente e também usá-lo preguiçosamente. A
sobrecarga também não é tão ruim, pois os dados são armazenados em
pedaços e, portanto, há apenas uma sobrecarga para o “ponteiro de dados”
por pedaço.
Se você olhar a documentação em Data.ByteString.Lazy,
verá que ele tem muitas funções com os mesmos nomes que as de
Data.List, apenas as assinaturas de tipo têm
ByteString e Word8 em vez de [a]
e a. Essas funções têm os mesmos nomes que as funções de
lista, então vamos importá-lo como qualfied.
import qualified Data.ByteString.Lazy as B
import qualified Data.ByteString as S
B contém tipos e funções de bytestring preguiçosos,
enquanto S contém os estritos. Usaremos principalmente a
versão preguiçosa.
A função pack tem a assinatura de
tipo pack :: [Word8] -> ByteString. Isso significa que é
preciso uma lista de bytes do tipo Word8 e retorna uma
ByteString. Você pode pensar nela como uma função que pega
uma lista, que é preguiçosa, e a torna menos preguiçosa, de modo que é
preguiçosa apenas em intervalos de 64k. Qual é o tipo
Word8? Bem, é como Int, só que tem um
intervalo muito menor, ou seja, 0-255. Representa um número de 8 bits.
E, assim como Int, está na classe de tipos
Num. Por exemplo, sabemos que o valor 5 é
polimórfico, pois pode ser qualquer tipo numérico. Bem, pode ser
Word8 também.
ghci> B.pack [99,97,110]
Chunk "can" Empty
ghci> B.pack [98..120]
Chunk "bcdefghijklmnopqrstuvwx" Empty
Como você pode ver, geralmente não precisamos nos preocupar com o
Word8, pois o sistema de tipos pode fazer com que os
números escolham esse tipo. Se você tentar criar esse caractere como um
número inteiro maior que 255, ele dará a volta (wrap
around).
ghci> B.pack [336, 337, 338]
Chunk "PQR" Empty
Também podemos descompactar (unpack) uma ByteString em uma
lista de bytes.
ghci> B.unpack $ B.pack [97..122]
[97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122]
fromChunks pega uma lista de
bytestrings estritas e a converte em uma bytestring preguiçosa. toChunks pega uma bytestring preguiçosa e
a converte em uma lista de estritas.
ghci> B.fromChunks [S.pack [40,41,42], S.pack [43,44,45], S.pack [46,47,48]]
Chunk "()*" (Chunk "+,-" (Chunk "./0" Empty))
Isso é bom se você tiver muitos bytestrings pequenos e deseja processá-los com eficiência (colocando-os em pedaços em uma única bytestring preguiçosa).
A versão bytestring de : é chamada cons. Leva um byte e uma bytestring e
coloca o byte no começo. É preguiçoso, por isso fará um novo pedaço,
mesmo que o primeiro pedaço na cadeia não esteja cheio. É por isso que é
melhor usar a versão estrita do cons, cons' se você estiver inserindo muitos
bytes.
ghci> B.cons 85 $ B.pack [80,81,82,84]
Chunk "U" (Chunk "PQRT" Empty)
ghci> B.cons' 85 $ B.pack [80,81,82,84]
Chunk "UPQRT" Empty
ghci> foldr B.cons B.empty [50..60]
Chunk "2" (Chunk "3" (Chunk "4" (Chunk "5" (Chunk "6" (Chunk "7" (Chunk "8" (Chunk "9" (Chunk ":" (Chunk ";" (Chunk "<" Empty))))))))))
ghci> foldr B.cons' B.empty [50..60]
Chunk "23456789:;<" Empty
Como você pode ver, empty faz uma bytestring vazia. Veja
a diferença entre cons e cons'? Com o
cons, temos um pedaço para cada byte. Com o
cons', temos apenas um pedaço com todos os bytes.
O módulo bytestring tem uma carga de funções que são análogas às em
Data.List, incluindo, mas não se limitando a,
head, tail, init,
null, length, map,
reverse, foldl, foldr,
concat, takeWhile, filter,
etc.
Ele também tem funções que têm o mesmo nome e se comportam da mesma
forma que algumas funções encontradas em System.IO, apenas
as Strings são substituídas por ByteStrings.
Por exemplo, o readFile em System.IO é do tipo
readFile :: FilePath -> IO String, enquanto o do módulo
de bytestrings é do tipo
readFile :: FilePath -> IO ByteString. Cuidado, se você
usar uma versão estrita de bytestring, ele tentará ler um arquivo na
memória de uma só vez! Com o bytestrings preguiçosos, ele lerá em
pedaços.
Vamos fazer um programa simples que aceite dois nomes de arquivo como
argumentos de linha de comando e copia o primeiro arquivo para o segundo
arquivo. Observe que System.Directory já tem uma função
chamada copyFile, mas vamos implementar nossa própria
função e usar bytreeStrings para que seja mais eficiente.
import System.Environment
import qualified Data.ByteString.Lazy as B
main = do
(fileName1:fileName2:_) <- getArgs
copyFile fileName1 fileName2
copyFile :: FilePath -> FilePath -> IO ()
copyFile source dest = do
contents <- B.readFile source
B.writeFile dest contents
Fazemos nossa própria função que recebe dois FilePaths
(lembre-se, FilePath é apenas sinônimo de
String) e retorna uma ação de E/S que copiará um arquivo
para outro usando bytestring. Na função main, apenas
obtemos os argumentos e chamamos nossa função com eles.
$ ghc --make bytestringcopy.hs
[1 of 1] Compiling Main ( bytestringcopy.hs, bytestringcopy.o )
Linking bytestringcopy ...
$ ./bytestringcopy something.txt ../../something.txt
Observe que um programa que não usa bytestrings poderia se parecer com isso:
import System.Environment
import System.IO
main = do
(fileName1:fileName2:_) <- getArgs
copyFile fileName1 fileName2
copyFile :: FilePath -> FilePath -> IO ()
copyFile source dest = do
contents <- readFile source
writeFile dest contents
A diferença é que usamos B.readFile e
B.writeFile em vez de readFile e
writeFile. Muitas vezes, você pode converter um programa
que usa strings para um programa que usa bytestrings, apenas importando
os módulos necessários e, em seguida, colocando o nome do módulo
qualificado na frente de algumas funções. Às vezes, você precisa
converter strings para bytestrings e vice-versa, mas isso não é
difícil.
Sempre que precisar ler muitos dados de um arquivo binário (como um arquivo de som ou uma imagem) ou um arquivo de texto enorme e a implementação normal da lista estiver lhe dando problemas de desempenho, tente usar bytestrings.
Exceções (Exceptions)

Todas as linguagens têm procedimentos, funções e pedaços de código
que podem falhar. Isso é um fato da vida. As coisas diferentes têm
maneiras diferentes de lidar com essas falhas. Em C, geralmente usamos
algum valor de retorno anormal (como -1 ou um ponteiro
nulo) para indicar que o que tentamos fazer falhou. Java e C#, por outro
lado, suportam exceções. Quando uma exceção é lançada, o fluxo de
controle salta para algum código de tratamento de exceção que definimos
e faz alguma limpeza (cleanup), e talvez relance a exceção para que
algum outro código de manipulação de erro possa cuidar disso também.
Haskell tem um sistema de tratamento de exceções muito bom. Exceções
podem ser lançadas de código puro ou de código impuro. No caso de código
puro, as exceções só podem ser capturadas na parte de E/S do nosso
código (porque não sabemos quando (ou se) algo será avaliado no código
puro, pois é preguiçoso e não tem uma ordem de execução bem definida,
enquanto o código de E/S tem). As exceções lançadas no código de E/S
podem ser capturadas no código de E/S. No entanto, na maioria das vezes,
não temos muito o que fazer quando nosso código puro lança uma exceção.
Pegue a função div, por exemplo: ela gera como resultado a
divisão inteira de seus argumentos. O div 4 2 retorna
2, mas o div 4 0 lançará uma exceção dizendo
que a divisão por zero aconteceu. Como o div é puro, não
existe uma ordem bem definida de quando ele será avaliado, portanto, não
sabemos quando e onde pegar essa exceção. Se tivermos que verificar
erros, geralmente usamos tipos como Maybe e
Either para indicar valores que são computados com sucesso
ou com falha.
Portanto, em Haskell, geralmente usamos exceções para as partes de E/S em nosso programa. Muitas coisas podem dar errado ao lidar com o mundo exterior, porque não é confiável. Por exemplo, tentamos abrir um arquivo e acontece que ele foi excluído ou não temos permissões para abri-lo e assim por diante.
Vimos como podemos apenas usar funções como
doesFileExist de System.Directory para
verificar se um arquivo existe, então não precisamos lidar com a
abertura de um arquivo que não existe. Usamos
doesFileExist, verificamos se é True ou
False e depois fazemos a coisa certa. Mas e se esse arquivo
realmente existir e quando vamos a abri-lo, ele não existe mais? Não há
certeza de que algum outro processo não o excluiu no tempo entre a
verificação de existência e a abertura do arquivo. É por isso que é
melhor usar exceções nesse caso. As exceções também são boas se o
procedimento puder falhar de várias maneiras e queremos apenas ter um
manipulador que pegue (catch) todas elas e faça alguma coisa.
Aqui está um código que abre um arquivo e conta quantas linhas há nele.
import System.Environment
import System.IO
main = do (fileName:_) <- getArgs
contents <- readFile fileName
putStrLn $ "The file has " ++ show (length (lines contents)) ++ " lines!"
Isso funciona bem, a menos que o arquivo não exista ou tenhamos apenas uma lista vazia de argumentos.
$ ghc --make linecount
[1 of 1] Compiling Main ( linecount.hs, linecount.o )
Linking linecount ...
$ ./linecount linecount.hs
The file has 7 lines!
$ ./linecount i_dont_exist.txt
linecount: i_dont_exist.txt: openFile: does not exist (No such file or directory)
Nós temos uma mensagem de erro do GHC dizendo que o arquivo não
existe. O erro aconteceu quando tentamos abrir o arquivo usando
readFile, mas note que readFile não abre o
arquivo quando a chamamos, mas apenas retorna uma ação de E/S que abrirá
o arquivo. A exceção foi lançada quando essa ação de E/S foi realizada.
Para corrigir esse erro, podemos usar a função catch do System.IO.Error. Seu
tipo é catch :: IO a -> (IOError -> IO a) -> IO a.
Leva duas ações de E/S. A primeira é a ação que tentaremos executar. A
segunda é o manipulador (handler). Se a primeira ação
lançar uma exceção, ela será passada para o manipulador, que então
decide o que fazer. Portanto, a ação que catch retorna fará
o mesmo que a primeira ação que passamos a ela, mas se algo der errado,
executará o manipulador. Vamos modificar nosso programa para usar
catch.
import System.Environment
import System.IO
import System.IO.Error
main = toTry `catch` handler
toTry :: IO ()
toTry = do (fileName:_) <- getArgs
contents <- readFile fileName
putStrLn $ "The file has " ++ show (length (lines contents)) ++ " lines!"
handler :: IOError -> IO ()
handler e = putStrLn "Whoops, had some trouble!"
Como você pode ver, em vez de fazer o que fizemos normalmente,
chamamos a função principal catch com toTry e
handler. toTry é a ação de E/S que tentamos
executar e handler é a função que recebe um
IOError e retorna uma ação a ser executada no caso de uma
exceção. Vamos ver se funciona.
$ ./linecount linecount.hs
The file has 7 lines!
$ ./linecount i_dont_exist.txt
Whoops, had some trouble!
No manipulador, não verificamos que tipo de IOError
obtivemos. Nós apenas dizemos “Ops, tive alguns problemas” para qualquer
tipo de erro. Assim como as definições de exceção em outras linguagens,
como Java, as exceções em Haskell têm uma hierarquia, de modo que
podemos verificar que tipo de erro ocorreu. Uma coisa útil que podemos
fazer é verificar se o erro que obtivemos é um erro de “arquivo não
existente” (does not exist error). Se for, dizemos ao usuário
que o arquivo não existe, mas se não for, lançamos a exceção novamente e
deixamos que ela faça o que quiser (como travar nosso programa). Aqui
está o manipulador atualizado:
handler :: IOError -> IO ()
handler e
| isDoesNotExistError e = putStrLn "The file doesn't exist!"
| otherwise = ioError e
A função isDoesNotExistError é um
predicado sobre IOErrors, o que significa que pega um
IOError e retorna True ou False.
Se for um erro de que o arquivo não existe, retornamos uma ação que
imprime uma mensagem agradável. Caso contrário, usamos a função ioError, que pega um IOError
e produz uma ação de E/S que lança esse erro. A ação terá o mesmo
resultado que a ação que falhou, mas nós a lançamos novamente para que o
usuário (ou o sistema operacional) saiba que algo de estranho aconteceu.
A função ioError tem o tipo
ioError :: IOError -> IO a, então o tipo a
pode ser o que quisermos. Isso ocorre porque ioError não
produzirá realmente um resultado, mas lançará uma exceção.
Existem vários predicados úteis que operam no
IOError:
isAlreadyExistsErrorisDoesNotExistErrorisAlreadyInUseErrorisFullErrorisEOFErrorisIllegalOperationisPermissionErrorisUserError
A maioria deles é autoexplicativa. isUserError avalia
como True quando usamos a função userError para fazer exceções, que é para
fazer exceções a partir do nosso próprio código e passá-las com
ioError. Por exemplo, você pode fazer
ioError $ userError "remote computer unplugged!".
Também podemos usar funções que começam com ioe para
obter alguns dados de um IOError.
ioeGetFileName:: IOError -> Maybe FilePathioeGetHandle:: IOError -> Maybe HandleioeGetLocation:: IOError -> String
Vamos usar ioeGetFileName para imprimir o caminho do
arquivo que causou o erro.
handler :: IOError -> IO ()
handler e
| isDoesNotExistError e =
case ioeGetFileName e of Just path -> putStrLn $ "Whoops! File does not exist at: " ++ path
Nothing -> putStrLn "Whoops! File does not exist at unknown location!"
| otherwise = ioError e
Tivemos que usar a expressão case aqui para verificar o
path que estava dentro do Maybe retornado por
ioeGetFileName. Geralmente, o IOError contém o
caminho do arquivo que causou o erro.
Exceções são legais, mas você não deve exagerar nelas. Ainda assim,
ao lidar com E/S e coisas que podem falhar a vontade, elas podem ser
muito úteis. Em código puro, tente usar tipos como Either e
Maybe para representar resultados que podem ter falhado.
Isso porque, com o uso de tipos, o verificador de tipos (que é seu
melhor amigo) pode ajudá-lo a encontrar erros no tempo da compilação e,
mesmo que seu código seja compilado, você tem certeza de que lidou com
tudo.
Parabéns, você agora sabe como lidar com E/S em Haskell! Embora, à
primeira vista, lidar com E/S possa parecer estranho, porque requer que
encadeemos ações de E/S, no final não é tão difícil e nos dá a
capacidade de separar as partes puras de nossos programas das impuras. A
seguir, falaremos mais sobre mônadas, que tornarão o E/S, o tratamento
de erros e muitas outras coisas mais claras. Você provavelmente também
vai finalmente entender o que são aqueles blocos do e por
que essa sintaxe funciona. Além disso, aprender mônadas é como obter o
privilégio de “usuário nível 10” em Haskell. Então vá em frente e
leia!