• Nenhum resultado encontrado

Arquivos, canais e descritores

No documento Programação Funcional Usando Haskell (páginas 191-195)

Os arquivos são considerados como variáveis permanentes, cujos valores podem ser lidos ou atualizados em momentos futuros, depois que o programa que os criou tenha terminada a sua execução. É desnecessário comentar a importância que estas variáveis têm para a computação. No entanto, é necessária uma forma de comunicação do programa com os arquivos. Já foi vista uma forma, através do comando do. A outra é através de descritores.

Para obter um descritor (do tipo Handle) de um arquivo é necessária a operação de abertura deste arquivo para que futuras operações de leitura e/ou escrita possam acontecer. Além disso, é necessária uma operação de fechamento deste arquivo, após suas operações terem sido realizadas, para que os dados não sejam perdidos quando o programa terminar sua execução.

A Tabela 7.1 mpstra as formas como os arquivos podem ser abertos, operados e fechados em Haskell, usando o comando openFile.

Tabela 7.1: Forma de operações com arquivos em Haskell. IOMode lê escreve Posição inicial Notas

ReadMode Sim Não Início do arquivo O arquivo deve existir

WriteMode Não Sim Início do arquivo Se existir o arquivo perde os dados ReadWriteMode Sim Sim Início do arquivo O arquivo é criado se não existir

senão os dados são conservados AppendMode Não Sim Fim do arquivo O arquivo é criado se não existir e

os dados existentes são conservados

Os programas em Haskell podem trabalhar com arquivos tipo texto ou com arquivos binários. Neste último caso, é necessário utilizar openBinaryFile em vez de openFile. O sistema opera- cional Windows processa arquivos textos de forma diferente como processa arquivos binários. O sistema operacional Linux utiliza tanto openFile quanto openBinaryFile, mas é aconselhável usar openBinaryFile para processar arquivos binários para efeito de portabilidade.

Estas operações são descritas em Haskell, da seguinte forma:

data IOMode = ReadMode | WriteMode | AppendMode | ReadWriteMode openFile :: FilePath -> IOMode -> IO Handle

hClose :: Handle -> IO ()

Por convenção, todos os comandos usados para tratar descritores de arquivos são iniciadas com a letra h. Por exemplo, as funções

hPutChar :: Handle -> Char -> IO () hPutStr :: Handle -> String -> IO () hPutStrLn :: Handle -> String -> IO () hPrint :: Show a => Handle -> a -> IO ()

são utilizadas para escrever alguma coisa em um arquivo. Já os comandos hGetChar :: Handle -> IO ()

hGetLine :: Handle -> IO ()

são utilizados nas operações de leituras em arquivos. As funções hPutStrLn e hPrint incluem um caractere ’\n’ para a mudança de linha ao final da string.

Haskell também permite que todo o conteúdo de um arquivo seja retornado como uma única string, através da função

hGetContents :: Handle -> String

No entanto, há que se fazer uma observação. Apesar de parecer que hGetContents retorna todo o conteúdo de um arquivo de uma única vez, não é realmente isto o que acontece. Na realidade, a função retorna uma lista de caracteres, como esperado, mas de forma lazy, onde os elementos são lidos sob demanda.

7.3.1 A necessidade dos descritores

Lembremos que um arquivo também pode ser escrito sem o uso de descritores. Por exemplo, pode-se escrever em um arquivo usando o comando

type FilePath = String

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

Também podemos acrescentar uma string ao final de um arquivo com o comando appendFile :: FilePath -> String -> IO ()

Então, qual a necessidade de se utilizar descritores? A resposta é imediata: eficiência. Vamos analisar.

Toda vez que o comando writeFile ou appendFile é executado, deve acontecer também uma seqüência de ações, ou seja, o arquivo deve ser inicialmente aberto, a string deve ser escrita e, finalmente, o arquivo deve ser fechado. Se muitas operações de escrita forem necessárias, então serão necessárias muitas destas seqüências. Ao se utilizar descritores, necessita-se apenas de uma operação de abertura no início e outra de fechamento no final.

Para ler ou escrever a partir de um descritor, que normalmente corresponde a um arquivo em disco, o sistema operacional mantém um registro interno da posição atual de leitura do arquivo, ou seja, um ponteiro para a posição atual. A cada vez que uma leitura é feita, o sistema operacional retorna o valor do dado apontado por este ponteiro e incrementa o ponteiro para apontar para o próximo dado a ser lido.

O comando hTell pode ser utilizado para ver a posição corrente deste ponteiro em relação ao início do arquivo, que tem a posição 0 (zero). Isto significa que, quando um arquivo é criado, ele é criado vazio e o ponteiro tem o valor 0 (zero). Quando se escrevem 10 bytes no arquivo, a posição será 10. hTell tem o tipo hTell :: Handle − > IO Integer.

O complemento de hTell é hSeek que permite trocar o valor da posição atual. Ele toma três parâmetros: Handle, SeekMode e um endereço. O parâmetro SeekMode pode ter três valores distintos para especificar como uma determinada posição deve ser interpretada. O primeiro valor é ]textbfAbsoluteSeek indicando que a posição deve ser a atual que também é a indicada pelo valor de hTell. O segundo valor é RelativeSeek que deve ser interpretado como um valor a partir da posição atual, onde um valor positivo indica uma posição para a frente da posição atual, um valor negativo indica uma posição para atrás da posição atual no arquivo. O terceiro valor é SeekFromEnd que especifica a quantidade de bytes antes do final do arquivo.

Vamos mostrar um exemplo baseado em O’Sullivan [29] que é apresentado em várias versões para se verificar as possibilidades que Haskell oferece, através das ações.

import System.IO

import Data.Char(toUpper) main :: IO ()

main = do entrada <- openFile "entra.txt" ReadMode saida <- openFile "sai.txt" WriteMode loop_principal entrada saida

hClose entrada hClose saida

loop_principal entrada saida =

do fim_de_arquivo <- hIsEOF entrada if fim_de_arquivo then return ()

else do inpStr <- hGetLine entrada

hPutStrLn saida (map toUpper inpStr) loop_principal entrada saida

A execução deste programa, como em qualquer outro em Haskell, se inicia com a função main. Neste caso, o arquivo "entra.txt" é aberto no modo de leitura e o arquivo "sai.txt" é aberto no modo de escrita. Em seguida o programa chama a função loop_principal que faz a leitura de uma linha do arquivo "entra.txt". Essa linha lida tem todos os seus caracteres trocados para caracteres maiúsculos e é escrita no arquivo "sai.txt". A função loop_principal é chamada recursivamente para um novo ciclo de leitura e processamento de mais uma linha, equivalendo a um loop em uma linguagem imperativa. Este ciclo é repetido até que o arquivo "entra.txt" atinja seu final. Finalmente os arquivos "entra.txt" e "sai.txt" são fechados.

Deve ser observado o papel da função return () em programas em Haskell que é diferente dos propósitos nas linguagens imperativas. Por exemplo, em C, esta função tem o propósito de encerrar a execução da função corrente. Em Haskell, a função return () tem significado oposto ao da função ->, ou seja, toma um valor puro e o coloca dentro de uma ação de IO. Por exemplo, sendo 10 um valor inteiro do tipo Integer, então return 10 cria uma ação armazenada em um valor do tipo IO Integer. Quando esta ação for executada, ela produz o valor 10.

Este mesmo programa pode ser feito de outra forma, usando hGetContents que é uma forma de leitura usando o mecanismo de leitura lasy empregado em Haskell.

import System.IO

import Data.Char(toUpper) main :: IO ()

main = do entrada <- openFile "entra.txt" ReadMode saida <- openFile "sai.txt" WriteMode inpStr <- hGetContents entrada

let resultado = processaDado inpStr hPutStr saida resultado

hClose entrada hClose saida

processaDado :: String -> String processaDado = map toUpper

Este programa ainda pode ser modificado, transformando-o em outro ainda mais compacto. import System.IO

import Data.Char(toUpper) main :: IO ()

main = do entrada <- openFile "entra.txt" ReadMode saida <- openFile "sai.txt" WriteMode inpStr <- hGetContents entrada

hPutStr saida (map toUpper inpStr) hClose entrada

A partir destes exemplos, pode-se observar que o comando hGetContents é utilizado como um filtro, onde os dados são lidos em um arquivo, processados e escritos em um outro arquivo. Este tipo de processamento é bastante comum e, por este motivo, foram criadas duas funções para atender a este demanda: readFile e writeFile. Estas duas funções gerenciam todos os detalhes de abertura, leitura, processamento e fechamento de arquivos como strings. A função readFile usa hGetContents internamente. Desta forma, o exemplo anterior pode ser construído ainda mais sinteticamente.

import Data.Char(toUpper) main :: IO ()

main = do inpStr <- readFile "entra.txt"

writeFile "saida.txt" (map toUpper inpStr)

7.3.2 Canais

Os descritores também podem ser associados a canais, que são portas de comunicação não asso- ciadas diretamente a um arquivo. Os canais mais comuns são: a entrada padrão (stdin), a área de saída padrão (stdout) e a área de erro padrão (stderr). As operações de I/O para caracteres e strings em canais incluem as mesmas listadas anteriormente para a manipulação de arquivos. Na realidade, as funções getChar e putChar são definidas como:

getChar = hGetChar stdin putChar = hputChar stdout

Até mesmo hGetContents pode ser usada com canais. Neste caso, o fim de um canal é sinalizado com um cartactere de fim de canal que, na maioria dos sistemas, é Ctrl-d.

Normalmente, um descritor é um arquivo, mas pode ser também uma conexão de rede, um drive ou um terminal. Para verificar se um dado descritor pode, ou não, ser varrido, pode-se usar o comando hIsSeekable que retorna um valor booleano.

No documento Programação Funcional Usando Haskell (páginas 191-195)