Haskell/Recursividade
Este módulo encontra-se em processo de tradução. A sua ajuda é bem vinda. |
Recursividade é uma forma de repetição que pode ser aplicada em muitos casos, não só da Matemática ou da Computação.[nota 1] Em Haskell, funções recursivas são fundamentais como veremos a diante.
Na Computação, diz-se que uma função é recursiva quando ela chama a si mesma. Ela geralmente possui pelo menos um caso base, que interrompe a recursividade, e um caso recursivo, que define a condição para que haja repetição. Ter um caso base bem definido é importante para impedir a repetição infinita da função.
Recursividade numérica
[editar | editar código-fonte]A melhor maneira de explicar recursividade é com exemplos claros.
A função fatorial
[editar | editar código-fonte]Na Matemática, mais especificamente em Análise Combinatória, usa-se bastante uma função chamada fatorial. É uma função que só pode ser aplicada a números não-negativos e inteiros: ela calcula o produto de todos os números inteiros menores que, ou igual ao argumento, e maiores que zero. O fatorial (representado por ) de 6 seria:
Se definirmos que o fatorial de 0 é igual a 1, isto é, que ,[nota 2] podemos definir de forma recursiva. Vejamos:
De forma generalizada:
Fica clara a recursvidade da função fatorial, pois ela é definida em termos de si mesma. E também são bastante evidentes os casos base e recursivo, e suas condições:
- Caso base: o fatorial de 0 é 1.
- Caso recursivo: o fatorial de qualquer outro número maior que zero, é igual a este número multiplicado pelo fatorial de seu predecessor.
Em Haskell, podemos escrever esta função usando em termos de si mesma usando casamento de padrões:
fatorial 0 = 1
fatorial n = n * fatorial (n - 1)
A primeira linha é o caso base, e a segunda, é o caso recursivo, que chama fatorial
novamente. Perceba o uso de parênteces na segunda linha: sem eles, Haskell interpretaria a segunda linha como sendo n * (fatorial n) - 1
.
Para testar, é recomendável escrever fatorial
num arquivo e carregá-lo no GHCi. Entretanto, por ser uma função simples, podemos escrever tudo numa linha só:[nota 3]
Prelude> let fatorial 0 = 1 ; fatorial n = n * fatorial (n - 1) Prelude> fatorial 6 720
É importante saber que não há nada de mágico acontecendo. Nós já vimos alguns exemplos de funções que chamam outras para obtermos um resultado final. Numa função recursiva, a diferença é que em vez de chamar uma sub-função diferente, ela chama a si mesma, e o que muda são os argumentos de entrada.
Vejamos o que acontece quando chamamos fatorial 3
:
- 3 não é 0 (o caso base), portanto, precisamos chamar
fatorial 2
- 2 não é 0, portanto, precisamos chamar
fatorial 1
- 1 não é 0, portanto, precisamos chamar
fatorial 0
- 0 é 0, portanto, chegamos ao caso base, e temos um valor numérico como resultado:
1
.
- 0 é 0, portanto, chegamos ao caso base, e temos um valor numérico como resultado:
- Agora retornamos ao cálculo de
fatorial 1
e podemos reescrever a expressão1 * fatorial 0
como sendo1 * 1
, pois já calculamosfatorial 0
.
- 1 não é 0, portanto, precisamos chamar
- Retornamos ao cálculo de
fatorial 2
. Com o resultado defatorial 1
podemos reescrever a expressão atual:2 * (1 * 1)
- 2 não é 0, portanto, precisamos chamar
- Chegamos ao cálculo de
fatorial 3
. Usando o resultado defatorial 2
, a expressão atual pode ser reescrita como(3 * (2 * (1 * 1)))
, cujo resultado final é6
.
Perceba que temos a repetição do valor 1
. Isso acontece porque definimos o caso base em 0. Poderíamos ter definido o caso base como fatorial 1 = 1
, mas, geralmente, é melhor seguir a definição matemática, em vez de simplificá-la.
- Exercício
- O que acontece se calcularmos
fatorial (-1)
? - Como já vimos antes, a ordem dos padrões é importante quando definimos uma função. Se fizéssemos e tentássemos calcular
fatorial n = n * fatorial (n - 1) fatorial 0 = 1
fatorial 6
, o que aconteceria? - O fatorial duplo de um número é a multiplicação dos sucessores intercalados, partindo de 1 (ou 2) até o argumento. Por exemplo: o fatorial duplo de 8 é 8 × 6 × 4 × 2 = 384; o fatorial duplo de 7 é 7 × 5 × 3 × 1 = 105. Defina uma função
fatorialDuplo
em Haskell que faça esta conta.
Laços de repetição, recursividade e parâmetros acumuladores
[editar | editar código-fonte]Em linguagens de programação imperativas, usam-se laços de repetição no mesmo contexto em que usamos recursividade em Haskell. Por exemplo, em C, poderíamos usar o laço for para escrever uma fatorial
da seguinte forma:
int fatorial(int n) {
int res = 1;
for ( ; n > 1 ; n--)
res = n * res;
return res;
}
O laço for faz com que res
seja multiplicada por n
repetidamente. Depois de cada repetição, a operação n--
é realizada, isto é, n
é decrescido de uma unidade. O laço é interrompido quando a condição n > 1
, não for mais válida, ou seja, quando n
já não for maior que 1
.
Não é possível criar uma versão parecida em Haskell, pois a linguagem não permite a alteração de valores de uma variável, como acontece com n
e res
na versão em C. Entretanto, é possível converter um laço num forma recursiva equivalente. Para isso, fazemos com que cada variável do laço torne-se um argumento de uma função recursiva. Por exemplo:
fatorial n = aux n 1
where aux n res
| n > 1 = aux (n - 1) (res * n)
| otherwise = res
aux
[nota 4] é uma função auxiliar. É ela que, de fato, realiza o cálculo, enquanto que fatorial
apenas a envolve. Na função aux
, temos dois argumentos: n
, que vem de fora, e é definido pelo usuário quando chama fatorial n
; e res
, que chamamos de parâmetro acumulador, pois ele acumula os resultados parciais do cálculo do fatorial, e depois é enviado para a saída de aux
na cláusula otherwise
.
Outras funções recursivas
[editar | editar código-fonte]Apesar de ser muito útil, não há nada de muito especial com a implementação de fatorial
, e também existem muitas outras funções recursivas na Matemática. Por exemplo, a própria multiplicação pode ser definida recursivamente. Sabemos que, para números inteiros, trata-se da repetição da operação de adição, isto é, 5 × 4 é o mesmo que 5 + 5 × 3, que é o mesmo que 5 + 5 + 5 × 2, e assim em diante. Portanto, em Haskell poderíamos definir a função mult
que realiza multiplicação desta forma:
mult _ 0 = 0 -- caso base: n * 0 = 0
mult n m = n + (mult n (n - 1)) -- caso recursivo: n + (n * (m-1))
Olhando estes exemplos de forma mais ampla, podemos ver como recursividade numérica se encaixa num padrão mais geral. O caso base geralmente consiste de um valor específico (geralmente 0 ou 1), no qual a resposta pode ser calculada imediatamente. O caso recursivo computa o resultado ao chamar a função de forma recursiva, mudando os argumentos a cada chamada, e manipulando o resultado de alguma forma para produzir uma resposta final. Os "argumentos diferentes" são geralmente uma unidade menor que o argumento anterior.
- Exercício
- Defina uma função recursiva chamada
potencia
tal que o resultado depotencia x y
sejax
elevado à potênciay
. - Lhe é dada a função
maisUm x = x + 1
. Sem usar(+)
novamente, e usando apenasmaisUm
e(-)
, defina a função recursivaadicao
tal queadicao x y
resulte na soma dex
ey
. Considere apenas números inteiros não-negativos. - Escreva a função
log2
que calcula o logaritmo de seu argumento na base 2. Por exemplo:log2 1024 = 10
,log2 1 = 0
,log2 11 = 3
. Dica: use números inteiros, e usediv x y
(uma função nativa de Haskell) para calcular o quociente da divisão dex
pory
.
Recrusividade usando listas
[editar | editar código-fonte]Haskell possui muitas funções recursivas, principalmente as que atuam sobre listas.[nota 5] Considere a função length
que, como já vimos, calcula o comprimento de uma lista. Ela pode ser definida de forma recursiva (lembre-se de casamento de padrões com listas):
length :: [a] -> Int
length [] = 0
length (x:xs) = 1 + length xs
length
desta forma no GHCi, quando usá-la, uma mensagem de erro será exibida alertando sobre "ocorrência ambígua". Isso quer dizer que a mesma função foi carregada duas vezes, o que faz sentido, já que o Prelude já possui uma length
. Para usar sua função, tente renomeá-la, como minhaLength
, ou length'
. Além disso, a length
contida no Prelude não é definida da mesma fora que fizemos aqui, mas isso não faz muita diferença para nós.A assinatura de tipo de length
nos diz que ela recebe qualquer tipo de lista e retorna um Int. Na linha seguinte, temos o caso base, que estipula que uma lista vazia tem comprimento igual a 0. A terceira e última linha é o caso recursivo: somar 1 ao comprimento da cauda da lista. Neste caso, length
será usada para calcular o comprimento de sucessivas caudas da mesma lista, cada vez mais curtas, até ser apenas uma lista vazia. Quando chegar a este ponto, uma soma de 1s e 0 será a expressão final que resultará no comprimento da lista de entrada.
Agora, vejamos a função concatenar, (++)
. Já sabemos como ela funciona:
Prelude> [1,2,3] ++ [4,5,6] [1,2,3,4,5,6]
No Prelude, ela é implementada recursivamente com ajuda de (:)
, o operador cons:
(++) :: [a] -> [a] -> [a]
[] ++ ys = ys
(x:xs) ++ ys = x : xs ++ ys
Apesar de parecer um pouco mais complicada que length
, ainda é um caso fácil de se entender. A anotação de tipo nos diz que ela recebe duas listas do mesmo tipo e retorna uma terceira lista também do mesmo tipo. O caso base diz que concatenar uma lista vazia e uma lista não-vazia, resulta na própria lista não-vazia. O caso recursivo cria uma expressão desmembrando a primeira lista até que ela esteja toda representada na notação (:)
, e adiciona a segunda lista ao fim da primeira, também com (:)
.
Podemos perceber um padrão nestas duas funções: em funções recursivas que envolvem listas, o caso base geralmente lida com uma lista vazia, e o caso recursivo manipula o dado da cabeça da lista, enquanto passa apenas cauda como argumento para a próxima etapa.
- Exercício
Escrevas as seguintes funções de forma recursiva. Note que todas elas já possuem uma versão nativa no Prelude. Você pode usar dois casos base, caso ache necessário, apesar de que todos podem ser resolvidos com apenas um.
replicar :: Int -> a -> [a]
, que recebe um contador n e um elemento, e retorna uma lista contendo n elementos iguais aos da entrada. Ex.:replicar 3 2 = [2,2,2]
. A versão do Prelude chama-sereplicate
.(!!!) :: [a] -> Int -> a
, que recebe uma lista e um índice de posição, e retorna o elemento contido em tal posição dentro da lista. O primeiro elemento da lista está na posição 0. Ex.:"abcde" !!! 2 = 'c'
. A versão do Prelude chama-se(!!)
.ziper :: [a] -> [b] -> [(a,b)]
, que age como um ziper entre duas listas, isto é, pega um elemento de cada lista, forma uma dupla, e retorna uma lista de duplas. Ex.:ziper [1,2,3,4] "abc" = [(1, 'a'), (2, 'b'), (3, 'c')]
. Perceba que a ação é interrompida quando o limite da lista mais curta é atingido. A versão do Prelude chama-sezip
.- Redefina
length
usando uma função interna auxiliar e um parâmetro acumulador.
Conhecendo outras funções elementares
[editar | editar código-fonte]Mesmo sendo extremamente importantes em Haskell, nem sempre escrevemos funções recursivas. Na verdade, geralmente usamos funções mais básicas contidas nas muitas bibliotecas disponíveis. Estas, por sua vez, são a responsáveis por lidar com a recursividade. Por exemplo, um programador experiente não escreveria fatorial
da mesma forma que fizemos aqui. Na verdade, ele usaria função product
que calcula o produto dos valores de uma lista:
fatorial n = product [1..n]
É claro que o funcionamento de product
envolve recursividade com listas, mas nós, os programadores ou usuários, sequer tivemos que lidar com a análise de casos base ou recursivos.[nota 6]
Notas
[editar | editar código-fonte]- ↑ Cf. Wikipédia.
- ↑ não é uma definição arbitrária. É um resultado necessário e importante, mas, acima de tudo, é válido, e que chamamos de produto vazio.
- ↑ Na verdade, pode-se escrever uma função diretamente no GHCi com quantas linhas precisarmos, bastanto separá-las usando ponto-e-vírgula.
- ↑ É mais comum que funções auxiliares em Haskell sejam chamadas de
go
(go significa "ir", ou "vai", em inglês), ouworker
("trabalhador", em inglês), do queaux
. - ↑ Este fato não é coincidência: sem variáveis mutáveis, recursividade é a única forma de implementarmos estruturas de controle para conseguirmos lidar com uma grande quantidade de dados. Isso pode parecer uma limitação da linguagem, mas a impressão só dura até que você se acostume.
- ↑ Na verdade, nem mesmo
product
é definida de forma recursiva, pois ela delega a tarefa da repetição para uma outra função, chamadafoldl
, que veremos nos capítulos seguintes.