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

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
Nota: Os parêntesis cercando a função de dentro são importantes. Sem eles, no primeiro exemplo, o compilador diria que 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

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

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

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 inverterPalavras realmente 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 words para criar uma lista de Strings dividindo um String contendo espaços em branco;
  • Usar reverse para inverter a ordem dos elementos de uma lista (que é exatamente o mesmo que a função auxiliar inverterLista faz em inverterPalavras), e;
  • Usar unwords para recriar um String a partir de uma lista de Strings usando espaços em branco (o oposto de words);

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

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.
  1. 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.
  2. Existem extensões na linguagem Haskell que permitem omitir o prelúdio, mas não cobriremos esse tema neste wikilivro.
  3. Este exemplo foi inspirado na demonstração Simple Unix tools da HaskellWiki.