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:
Em Haskell:
Operando com listas
Em JavaScript:
Em Haskell:
Condicionais
Em JavaScript:
Em Haskell:
Definindo funções
Em JavaScript:
Em Haskell:
Usando funções
Em JavaScript:
Em Haskell:
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:
Construímos [1, 2, 3]
com:
Note que isso é equivalente ao uso do :
em Haskell:
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:
E usar nossas definições
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:
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:
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:
E em Haskell:
E porque a linguagem é lazy, podemos bem fazer:
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:
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:
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:
Se isso já estivesse computado, poderíamos pensar em:
E se pensarmos em todos os elementos menos o primeiro de fibs
; o resultado de
tail fibs
:
Não sei se é fácil de enxergar isso, mas tail (tail fibs)
é:
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:
Com tudo isso. Podemos escrever fibonacci como:
Declaramos uma lista infinita definida em termos dela mesma! E podemos a usar com:
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.