Implementando fibonacci em Haskell
por Pedro Tacla Yamada
A mágica da avaliação não estrita e funções de alta ordem. Um crash course de 5 minutos em Haskell e a descontrução da implementação de fibonacci clássica em uma linha com comparações de trechos de JavaScript.
Muitos que se aventuraram em aprender Haskell devem ter se deparado com a implementação da sequência de Fibonacci em uma linha e com complexidade linear. Vamos a desconstruir, já que isso ainda não foi feito em português.
Haskell em 5 minutos
Haskell é uma linguagem de programação funcional, não estrita, pura e fortemente “tipada”. Isso quer dizer que é uma linguagem onde funções são membros de primeira ordem, onde a avaliação é “lazy” por padrão, onde o IO do programa precisa estar isolado da lógica, sem loops, sem variáveis e onde todos os valores tem tipos (o compilador reclama quando algo não faz sentido). A ausência de loops e variáveis é contornável de duas formas:
-
Recursão
-
Variáveis isoladas em “objetos” especiais ou para manter estado em um programa concorrente ou para ter uma performance maior
Operando com números
Em JavaScript:
// "//" é um comentário
1 + 10 // => 11
2 * 3 // => 6
Em Haskell:
-- "--" é um comentário
1 + 10 -- => 11
2 * 3 -- => 6
Operando com listas
Em JavaScript:
[1, 2, 3, 4] // => [1, 2, 3, 4]
[1, 2] // => [1, 2]
[1, 2][0] // => 1
[1, 2][1] // => 2
Em Haskell:
[1, 2, 3, 4] -- => [1, 2, 3, 4]
[1, 2] -- => [1, 2]
[1, 2] !! 0 -- => 1
[1, 2] !! 1 -- => 2
-- Adicionando um elemento na frente da lista (haskell-only):
1 : [2, 3] -- => [1, 2, 3]
Condicionais
Em JavaScript:
if(something) {
// ...
} else if(somethingElse) {
// ...
} else {
// ...
}
Em Haskell:
if something
then -- ...
else if somethingElse
then -- ...
else -- ...
Definindo funções
Em JavaScript:
function soma10(numero) { return numero + 10 }
function elevaAoQuadrado(numero) { return numero * numero }
Em Haskell:
soma10 numero = numero + 10
elevaAoQuadrado numero = numero * numero
Usando funções
Em JavaScript:
soma10(2) // => 12
elevaAoQuadrado(10) // => 100
Em Haskell:
soma10 2 -- => 12
elevaAoQuadrado 10 -- => 100
Paralelo com a biblioteca lodash
A biblioteca lodash
tem uma série de funções com equivalentes no Haskell.
Algumas que valem a pena mencionar:
_.head
/_.first
equivale ahead
_.tail
/_.rest
equivale atail
_.map
equivale amap
_.reduce
equivale afoldl
Por que digo “listas” e não Array
s?
Em Haskell, por padrão, o literal
[1, 2, 3]
é uma “lista” e não um Array
.
Isso quer dizer que quando escrevo [1, 2, 3]
estou denominando a estrutura de
dados “lista ligada”. Ou seja uma lista é um elemento e uma referência para uma
outra lista com o resto da lista (!). Em JavaScript:
var vazia = {};
function lista(x, resto) {
return { primeiro: x, resto: resto, };
}
function primeiro(lista) { return lista.primeiro; }
function resto(lista) { return lista.resto; }
function index(lista, n) {
if(lista === vazia) return null;
else if(n === 0) return lista.primeiro;
else return index(lista.resto, n - 1);
}
function length(lista) {
var len = 0;
while(lista !== vazia) {
len += 1;
lista = lista.resto;
}
return len;
}
Construímos [1, 2, 3]
com:
lista(1, lista(2, lista(3, vazia)));
Note que isso é equivalente ao uso do :
em Haskell:
1 : 2 : 3 : []
Na verdade o :
pode ser considerado como um sugar para algo equivalente a
lista
em JavaScript e o []
um alias para algo equivalente a vazia
no
código acima. Podemos inclusive definir isso, apesar de que entrar nos detalhes
de definição de operadores ou estruturas de dados não é o intuito desse post:
vazia = []
lista x resto = x : resto
E usar nossas definições
lista 1 (lista 2 (lista 3 vazia))
Algumas propriedades dessa característica
length
,index
epush
são O(n)lista(10, listaExistente)
nos permite concatenar um elemento no começo da lista em O(1)primeiroElemento
eresto
são O(1)- Podemos modelar isso para que nossos algoritmos não sejam destrutíveis (para que as estruturas sejam imutáveis)
Avaliação preguiçosa

Uma das coisas fundamentais do Haskell é a avaliação preguiçosa. No fundo, Haskell é uma linguagem de boa. Ela não faz nada que não precisa. Então quando se escreve:
let x = soma10 200
O nome x
se refere a computação soma10 200
e não seu resultado. Quando
precisamos do valor (seja para imprimir no console ou mandar como uma
resposta HTTP) é que ele e todos os valores dos quais depende são computados.
Algo parecido agora faz parte da popular biblioteca lodash
para o JavaScript.
Quando chamo:
var numerosPrimosAte100 = _(_.range(0, 100)).filter(ehPrimo);
Nada acontece. Somente quando numerosPrimosAte100.value()
é chamado que a
computação ocorre. Isso abre portas para programas em estilo funcional com mais
expressividade sem desperdiçar iterações. No caso de Haskell abre portas para
listas infinitas. A notação de ranges usando lodash
e Haskell:
_.range(0, 5)
// => [0, 1, 2, 3, 4]
_.range(0, 10, 2)
// => [ 0, 2, 4, 6, 8 ]
// de 0 a 9 com 2 de intervalo
E em Haskell:
[0..4]
-- => [0, 1, 2, 3, 4]
[0,2..9]
-- => [0, 2, 4, 6, 8]
-- de 0 a 9 com 2 de intervalo
-- (basta escrever [n1, n2, n3..nLimite])
E porque a linguagem é lazy, podemos bem fazer:
[0..]
-- => [0, 1, 2, 3, 4, 5, 6, ...]
Se tentarmos imprimir isso, o programa não acaba nunca de escrever números na
tela. Mas se armazenarmos em um nome, podemos operar sobre a lista sem
problemas. Um exemplo usando o operador !!
para acessar o 10 número dessa
sequência infinita:
let x = [0..]
print (x !! 10) -- => 10
Implementando fibonacci
A sequência de fibonacci é definida pela Wikipédia como:
uma sequência de números inteiros, começando normalmente por 0 e 1, na qual, cada termo subsequente (número de Fibonacci) corresponde a soma dos dois anteriores
Como uma fórmulinha:
Uma implementação ingênua imperativa JavaScript:
function fibonacci(len) {
var ret = [0, 1];
for(var i = 2; i < len; i++) {
ret[i] = ret[i - 1] + ret[i - 2];
}
return ret;
}
fibonacci(5); // => [ 0, 1, 1, 2, 3 ]
Digo ingênua só porque calcular fibonacci(10)
e em seguida fibonacci(100)
não reutiliza resultados existentes.
Olhando para a lista
Podemos pensar em todos os elementos menos os dois primeiros da lista, como sendo todos os elemtos da lista somados a todos os elementos menos o primeiro elemento da lista.
wat?
Veja só, pense que temos uma lista infinita:
fibs = 0 : 1 : restoDeFibs
Se isso já estivesse computado, poderíamos pensar em:
fibs -- => 0, 1, 1, 2, 3, 5, ...
E se pensarmos em todos os elementos menos o primeiro de fibs
; o resultado de
tail fibs
:
fibs -- => 0, 1, 1, 2, 3, 5, ...
tail fibs -- => 1, 1, 2, 3, 5, 8, ...
Não sei se é fácil de enxergar isso, mas tail (tail fibs)
é:
fibs -- => 0 , 1 , 1 , 2 , 3 , 5 , ...
tail fibs -- => 1 , 1 , 2 , 3 , 5 , 8 , ...
tail (tail fibs) -- => (0 + 1), (1 + 1), (1 + 2), (2 + 3), (3 + 5), (5 + 8), ...
tail (tail fibs) -- => 1 , 2 , 3 , 5 , 8 , 13 , ...
A lista fibs
é definida como 0 : 1 : (somaDasListas fibs (tail fibs))
! Nosso
restoDeFibs
é igual a somaDasListas fibs (tail fibs)
.
O que nos leva a função somaDasListas
. Para todos os elementos na lista 1,
soma o mesmo elemento da lista 2. A implementação clássica usa a função
zipWith
que tem um equivalente no lodash
_.zipWith
. Vamos precisar de
outra função chamada null
que retorna true
se a lista estiver vazia. A ideia
é dada uma função fn
, uma lista1
e uma lista2
aplicamos fn
elementoDaLista1 elementoDaLista2
para todos os elementos das listas (supondo
que tenham o mesmo tamanho) e retornamos uma lista com os resultados. Então:
zipWith fn lista1 lista2 =
-- Se lista1 ou lista2 são vazias, retornamos a lista vazia:
if (null lista1) || (null lista2)
then []
-- Senão retornamos `fn` aplicado ao primeiro elemento de cada lista e
-- `zipWith fn` aplicado ao resto de cada lista
else (fn (head lista1) (head lista2)) :
zipWith fn (tail lista1) (tail lista2)
Com tudo isso. Podemos escrever fibonacci como:
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
Declaramos uma lista infinita definida em termos dela mesma! E podemos a usar com:
fibs !! 10 -- => 55
Amarrando as pontas
Se você chegou até aqui, obrigado. Muito desse post tentou ser o mais simples possível para pessoas que nunca viram Haskell, então não, isso não é como Haskell se parece em produção. Mesmo assim, é divertido ver o que é possível com a linguagem e talvez isso tenha te animado para a estudar.
Esse foi o primeiro post do blog da HaskellBR. Dúvidas comentários e posts são aceitos no repositório do GitHub: https://github.com/haskellbr/blog. No momento a meta é ter conteúdo publicado semanalmente.