Haskell/Composição de funções
| Este módulo encontra-se em processo de tradução. A sua ajuda é bem vinda. |
Neste capítulo, discutiremos a importância de nos familiarizarmos com funções e bibliotecas básicas de Haskell para escrever programas mais simples e confiáveis. Para isto, usamos composição de funções para criar funções complexas a partir de partes mais simples.
A composição de funções
[editar | editar código]Compor funções significa aplicar uma função a um argumento e, depois, aplicar outra função ao resultado da primeira função. Veja:
f x = x + 3
quadrado x = x ^ 2
Podemos compor f e quadrado de duas maneiras:
Prelude> quadrado (f 1) -- f primeiro, quadrado depois 16 Prelude> quadrado (f 2) 25 Prelude> f (quadrado 1) -- quadrado primeiro, f depois 4 Prelude> f (quadrado 2) 7
quadrado estaria sendo aplicado a dois argumentos, f e 1, o que resultaria num erro, pois quadrado recebe apenas um argumento.Compor duas ou mais funções resulta numa nova função. Se for o caso de sempre usarmos quadrado e f de forma composta, poderíamos definir novas funções para facilitar a programação:
quadradoDef x = quadrado (f x)
fDeQuadrado x = f (quadrado x)
Há também um outro jeito de compor duas funções: usando o operador de composição (.). Basta adicionar um ponto entre o nome de duas funções:
quadradoDef x = (quadrado . f) x
fDeQuadrado x = (f . quadrado) x
Note que as funções ainda são aplicadas da direita para a esquerda: (g . f) x == g(f(x)). O símbolo (.) foi escolhido para representar composição de funções por ser semelhando à notação matemática: .
Outra simplificação: nossas funções são como expressões matemáticas. Isso quer dizer que às vezes podemos então escrever
quadradoDef x = (quadrado . f) x
e eleminar x de ambos os lados, resultando em:
quadradoDef = quadrado . f
Aprenderemos mais sobre funções "sem argumentos" mais tarde. Mas é importante lembra-se desta possibilidade em Haskell desde o começo.
Porque incrementar seu "vocabulário" de funções
[editar | editar código]Em Haskell, é bastante simples criar funções compostas e definir variáveis. Mas é claro que antes de compor, precisamos ter as funções elementares em primeiro lugar. Enquanto as funções que criamos podem ser armazenadas e usadas sempre que quisermos, o próprio GHC já possui uma vasta quantidade de bibliotecas (pacotes) com muitas funções nativas prontas para serem usadas. Por isso, para se tornar um programador bem familiarizado com Haskell, é preciso conhecer algumas bibliotecas essenciais. Ou pelo menos, saber como encontrar as que você precisa.
Se usássemos o conteúdo contido no capítulo sobre Recurssão deste livro, já seria possível escrever qualquer tipo de programa para manipular listas, por exemplo. Entretanto, teríamos que escrever muitas funções bastante comuns — e que já estão presentes em algum pacote —, o que tornaria bastante ineficiente e custosa a tarefa de desenvolver certas aplicações. Por isso, ao longo deste wikilivro vamos nos aprofundar cada vez mais nas bibliotecas já disponíveis na comunidade Haskell, principalmente as já contidas no GHC.
O prelúdio e as bibliotecas
[editar | editar código]Antes de prosseguirmos, algumas informações sobre as bibliotecas de Haskell.
Primeiramente, Prelude (prelude é "prelúdio" em inglês) é a principal biblioteca disponível no GHC, e é a única que é carregada automaticamente em qualquer programa[nota 1] (a menos que você não queira).[nota 2] Além dos tipos de dados mais básicos, ela fornece muitas outras ferramentas bastante usadas: fst, snd, head e tail são alguns exemplos.
Outras bibliotecas são também chamadas de módulos, que podem ser importados no programa. Mais adiante, em capítulos futuros, falaremos sobre o desenvolvimento de módulos e como eles funcionam. Por enquanto, vamos focar em importar alguns módulos básicos. A função permutations, por exemplo, gera todas as permutações possíveis dos elementos de uma lista. Esta operação pode ser bastante útil, entertanto a função não está disponível no Prelude, mas no módulo Data.List. Para importar o módulo de listas (ou qualquer outro) e poder usar suas funções, você deve adicionar uma chamada no topo do seu arquivo .hs:
import Data.List
testarPermutations = permutations "Prelude"
Para carregar um módulo numa sessão do GHCi, deve-se usar o comando :module + (ou :m +):
Prelude> :m + Data.List Prelude Data.List> :t permutations permutations :: [a] -> [[a]]
Exemplo ilustrativo
[editar | editar código]Antes de seguirmos para o próximo capítulo, vejamos um exemplo das vantagens de se usar funções nativas (ou contidas em outros módulos).[nota 3] Suponha que você queira uma função que inverta a ordem das palavras contidas num String. Por exemplo, se a entrada fosse "O rato roeu a roupa do rei", a saída seria "rei do roupa a roeu rato O". Uma possível solução seria (não tente entender seu funcionamento, é apenas um caso ilustrativo):
inverterPalavras :: String -> String
inverterPalavras entrada = reunirNaoInvertidas (dividirRevertidas entrada)
where
dividirRevertidas s = go1 [] s
where
go1 divididas [] = divididas
go1 [] (c:cs)
| testarEspaco c = go1 [] cs
| otherwise = go1 [[]] (c:cs)
go1 (w:ws) [c]
| testarEspaco c = (w:ws)
| otherwise = ((c:w):ws)
go1 (w:ws) (c:c':cs)
| testarEspaco c =
if testarEspaco c'
then go1 (w:ws) (c':cs)
else go1 ([c']:w:ws) cs
| otherwise = go1 ((c:w):ws) (c':cs)
testarEspaco c = c == ' '
reunirNaoInvertidas [] = []
reunirNaoInvertidas [w] = inverterLista w
reunirNaoInvertidas strings = go2 (' ' : inverterLista primeiraPalavra) (outrasPalavras)
where
(primeiraPalavra : outrasPalavras) = inverterLista strings
go2 reunidas ([]:[]) = reunidas
go2 reunidas ([]:(w':ws')) = go2 (reunidas) ((' ':w'):ws')
go2 reunidas ((c:cs):ws) = go2 (c:reunidas) (cs:ws)
inverterLista [] = []
inverterLista w = go3 [] w
where
go3 rev [] = rev
go3 rev (c:cs) = go3 (c:rev) cs
Temos muitos problemas aqui:
- Primeiro, para saber se
inverterPalavrasrealmente funciona, você pode tentar entender seu funcionamento, testar exaustivamente com muitos exemplos, ou simplesmente acreditar. Pela sua extensão da função, já fica claro que não vale a pena tentar entender. E ficar testando funções de outras pessoas é uma tarefa nada produtiva para um programador, a não ser que seja este o seu trabalho. - Segundo, depois de escrever uma função tão extensa, se tiver que fazer alguma alteração ou corrigir algum bug mais tarde, você, com certeza, terá que inspecioná-la de cima a baixo, tentando relembrar e entender o que você fez, e procurando onde a mudança desejada deve ser feita.
- Por último, só olhando por cima já é possível ver um possível problema: temos uma função auxiliar chamada
testarEspaco, que verifica se um caractere é um espaço em branco que separa palavras. Ela, entretanto, testa apenas, e não considera tabulações ou quebra de linhas, por exemplo.
Felizmente, supondo que já estamos familiarizados com as funções contidas em Prelude, poderíamos:
- Usar
wordspara criar uma lista de Strings dividindo um String contendo espaços em branco; - Usar
reversepara inverter a ordem dos elementos de uma lista (que é exatamente o mesmo que a função auxiliarinverterListafaz eminverterPalavras), e; - Usar
unwordspara recriar um String a partir de uma lista de Strings usando espaços em branco (o oposto dewords);
Conhecendo todas essas funções e usando o operador (.), poderíamos escrever:
novaInverterPalavras :: String -> String
novaInverterPalavras entrada = (unwords . reverse . words) entrada
Você consegue ver as vantagens?
- É uma implementação bem mais curta.
- Sendo mais curta, é mais fácil inspecionar suas partes, se for preciso.
- Como todas estas funções usadas estão presentes em
Prelude, elas são confiáveis, pois já foram testas exaustivamente por milhares de pessoas.
A mensagem final é esta: se você se vir escrevendo funções gigantescas como inverterPalavras, procure outros recursos para facilitar seu trabalho, isto é, procure funções em outras bibliotecas e use composição para construir sua função final.
Principais fontes de recurso em Haskell
[editar | editar código]Um bom programador deve saber onde buscar os recursos que achar necessário para seu trabalho. Talvez você ainda não seja experiente o bastante para entender todo tipo de informação que possa aparecer, mas eis alguns lugares importantes na comunidade Haskell:
- Primeiramente, a documentação do próprio
Prelude, e também das bibliotecas contidas no GHC. - Hoogle é uma ferramenta de busca. Com ela pode-se fazer pesquisas na documentação de Haskell: buscar nomes de funções, tipos de dados, e até assinaturas de funções.
- Hackage é o principal arquivo de módulos e bibliotecas da comunidade Haskell, e ele geralmente pode ser acessado via cabal, o gerenciador de pacotes padrão de Haskell.
Notas
[editar | editar código]- ↑ Lembra-se de que toda linha do GHCi começa com Prelude>? Isso significa que o prelúdio foi carregado e está disponível para uso.
- ↑ Existem extensões na linguagem Haskell que permitem omitir o prelúdio, mas não cobriremos esse tema neste wikilivro.
- ↑ Este exemplo foi inspirado na demonstração Simple Unix tools da HaskellWiki.