Ir para o conteúdo

Entrada/Saída (I/O) e Programação Interativa

Até este ponto, focamos puramente em expressões funcionais que recebem dados e computam saídas sem qualquer efeito colateral. No entanto, um programa útil na vida real precisa interagir com o mundo externo: ler dados do teclado, gravar arquivos em disco ou realizar requisições de rede. Neste capítulo, estudaremos como o Haskell resolve o paradoxo de interagir com o mundo impuro de forma segura através da Monad de IO e da notação do.


1. O Paradoxo do I/O na Programação Funcional Pura

Se o Haskell é uma linguagem funcional pura baseada em determinismo matemático (onde a mesma função com os mesmos argumentos sempre retorna o mesmo valor), como podemos implementar uma função como lerTeclado? Se o usuário digita coisas diferentes a cada execução, essa função seria impura e violaria as garantias de otimização e avaliação preguiçosa do compilador.

Para resolver este paradoxo, o Haskell separa rigorosamente o mundo das expressões puras do mundo das ações de I/O.

  ┌────────────────────────────────┐
  │      Mundo Puro (Haskell)      │
  │  - Sem efeitos colaterais      │
  │  - Determinismo absoluto       │
  └───────────────┬────────────────┘
                  │  (Encapsulamento via Tipo IO)
  ┌────────────────────────────────┐
  │       Mundo Impuro (I/O)       │
  │  - Leitura/Escrita de Arquivos │
  │  - Interação com Teclado/Tela  │
  └────────────────────────────────┘

2. A Solução: O Tipo IO a

Em Haskell, uma ação que interage com o mundo externo tem o tipo IO a. Isso significa: "uma receita/ação que, quando executada pelo sistema operacional, realizará efeitos colaterais e retornará um valor do tipo a".

  • IO Char: Uma ação que realiza I/O e retorna um caractere (ex: getChar).
  • IO (): Uma ação que realiza I/O mas não retorna nenhum valor útil (representado pelo tipo unitário (), semelhante ao void de outras linguagens).

Info

Existe uma diferença crucial entre o tipo String e o tipo IO String. Um valor do tipo String é apenas texto puro que pode ser avaliado com segurança. Um valor do tipo IO String é uma ação pendente (como ler uma linha do teclado) que só produzirá a string quando for executada. Você nunca pode "extrair" um valor de IO para o mundo puro sem que a função inteira também se torne uma ação de IO.


3. Ações Básicas de I/O

A biblioteca padrão do Haskell fornece as seguintes ações primitivas de I/O:

  • getChar :: IO Char: Lê um único caractere do teclado.
  • putChar :: Char -> IO (): Escreve um único caractere na tela.
  • getLine :: IO String: Lê uma linha inteira de texto do teclado.
  • putStrLn :: String -> IO (): Escreve uma string na tela seguida por uma nova linha.

4. Sequenciando Ações com a Notação do

Para realizar várias ações de I/O em sequência, utilizamos o construtor sintático do. Ele nos permite encadear ações de forma linear, assemelhando-se à programação imperativa:

interagir :: IO ()
interagir = do
  putStrLn "Qual eh o seu nome?"
  nome <- getLine
  putStrLn ("Bem-vindo ao Haskell, " ++ nome ++ "!")

O Operador de Atribuição de I/O (<-)

Note o uso do símbolo <-. Ele serve para extrair o valor puro produzido por uma ação de IO e vinculá-lo a um nome local (neste caso, extraindo a String de dentro de IO String retornada por getLine).

O Significado de return em Haskell

Diferentemente de linguagens imperativas, onde return é um comando de controle que interrompe a execução de uma função, em Haskell o return é uma função comum.

A função return :: a -> IO a pega um valor puro de tipo a e o envelopa em uma ação de IO que não realiza nenhum efeito colateral:

pedirConfirmacao :: IO Bool
pedirConfirmacao = do
  putStrLn "Deseja continuar? (s/n)"
  resposta <- getChar
  if resposta == 's' 
    then return True 
    else return False

Por Baixo do do: os Operadores >>= e >>

A notação do é apenas açúcar sintático. Por baixo, as ações são encadeadas com dois operadores:

  • (>>=) (pronunciado bind): executa a ação da esquerda, extrai seu resultado e o passa para a função da direita. Tipo: IO a -> (a -> IO b) -> IO b.
  • (>>): executa a ação da esquerda, descarta o resultado e executa a da direita.

O programa interagir acima é equivalente a:

interagir :: IO ()
interagir =
  putStrLn "Qual eh o seu nome?" >>
  getLine >>= \nome ->
  putStrLn ("Bem-vindo ao Haskell, " ++ nome ++ "!")

Cada linha x <- acao do bloco do vira acao >>= \x -> ..., e cada ação sem <- vira um >>. Saber essa correspondência ajuda a ler código Haskell mais avançado e a entender o que a monad IO realmente faz.


5. Manipulação de Arquivos

Além de ler e escrever no console, o Haskell fornece ações básicas para ler e gravar arquivos em disco:

  • readFile :: FilePath -> IO String: Abre e lê todo o conteúdo de um arquivo de forma preguiçosa.
  • writeFile :: FilePath -> String -> IO (): Grava uma string em um arquivo (sobrescrevendo o conteúdo existente).
  • appendFile :: FilePath -> String -> IO (): Anexa uma string no final de um arquivo existente.

Exemplo de um programa que lê um arquivo e grava seu conteúdo em maiúsculas em outro arquivo:

import Data.Char (toUpper)

converterArquivo :: FilePath -> FilePath -> IO ()
converterArquivo origem destino = do
  conteudo <- readFile origem
  let conteudoMaiusculo = map toUpper conteudo
  writeFile destino conteudoMaiusculo
  putStrLn "Arquivo processado com sucesso!"

Note o padrão típico dos programas Haskell: a ação impura (readFile/writeFile) fica nas bordas, enquanto a transformação em si (map toUpper) é uma função pura, ligada com let — fácil de testar isoladamente.


6. O Padrão interact: Filtros de Texto em Uma Linha

Para programas do estilo "lê da entrada padrão, transforma, escreve na saída" (filtros Unix), o Haskell oferece a função interact :: (String -> String) -> IO (), que recebe uma função pura de transformação e cuida de todo o I/O. Um contador de linhas completo:

-- WC.hs
main = interact wordCount
    where wordCount input = show (length (lines input)) ++ "\n"
$ runghc WC < arquivo.txt
7

Graças à avaliação preguiçosa, o interact processa a entrada de forma incremental — funciona até com entradas maiores que a memória. Esse é o desenho recomendado para programas Haskell: concentre a lógica em funções puras e reduza o código impuro ao mínimo indispensável. Grande parte do risco em software está na comunicação com o mundo exterior; como o sistema de tipos nos diz exatamente quais partes do código têm efeitos colaterais, a "superfície de ataque" fica pequena e bem vigiada.


7. Argumentos de Linha de Comando e o Padrão "Núcleo Puro, Casca Impura"

Programas reais frequentemente processam arquivos indicados na linha de comando, em vez de usar a entrada/saída padrão. O módulo System.Environment fornece getArgs :: IO [String], que lê os argumentos passados ao executável. Um pequeno framework reutilizável para "ler um arquivo, transformar, gravar em outro arquivo":

import System.Environment (getArgs)

interactWith :: (String -> String) -> FilePath -> FilePath -> IO ()
interactWith function inputFile outputFile = do
  input <- readFile inputFile
  writeFile outputFile (function input)

main :: IO ()
main = mainWith minhaFuncao
  where
    mainWith function = do
      args <- getArgs
      case args of
        [entrada, saida] -> interactWith function entrada saida
        _ -> putStrLn "erro: são necessários exatamente dois argumentos"

    -- troque "id" pela função que você quiser testar
    minhaFuncao = id

Compilando e executando (em um executável Stack, este seria o conteúdo de app/Main.hs):

$ stack build && stack exec meu-programa-exe entrada.txt saida.txt

O ponto pedagógico é o design, não a sintaxe nova: interactWith é o "framework" fixo (I/O), e minhaFuncao :: String -> String é a única peça que muda de programa para programa — e é pura, portanto testável no GHCi sem tocar em disco algum. Esse é o mesmo padrão do capítulo de funções de alta ordem: isolar a lógica de negócio da mecânica de E/S.

No próximo capítulo, praticaremos esses conceitos avançados de Haskell através de uma coletânea de exercícios sobre tipos e entrada/saída.


Nota de atribuição: partes deste capítulo adaptam material de Real World Haskell, de Bryan O'Sullivan, Don Stewart e John Goerzen (book.realworldhaskell.org), sob a licença Creative Commons Attribution-Noncommercial 3.0.