Clojure: Entendendo (Infinite) Lazy Sequences
Ao iniciar recentemente meus estudos em Clojure enfrentei alguma dificuldade para entender o funcionamento de sequências infinitas. Especificamente, o simples código a seguir (retirado do livro Clojure For The Brave And True) me deixou intrigado:
(defn even-numbers
([] (even-numbers 0))
([n] (cons n (lazy-seq (even-numbers (+ n 2))))))
(take 10 (even-numbers))
; => (0 2 4 6 8 10 12 14 16 18)
O objetivo do código é simples: ele define uma função even-numbers
que retorna uma sequência infinita de números pares. Mas a implementação me deixou confuso…
A primeira coisa que me intrigou foi a chamada recursiva na função. Isso não deveria resultar em um stack overflow??? Afinal, a função não parece conter nenhuma condição de parada…
Depois de quebrar um pouco a cabeça, concluí (com razão) que eu ainda não possuía o domínio básico da linguagem necessário para compreender o que estava se passando. Decidi prosseguir com a leitura do livro e retornar depois.
Primeira pista: lazy-seq é uma Macro
Após ler o capítulo de Macros achei que finalmente poderia compreender o que estava se passando. Acontece que o operador lazy-seq
é uma Macro, não uma função.
Uma das principais diferenças entre macros e funções é que as funções tem seus argumentos completamente avaliados antes de serem invocadas; Já as macros recebem seus argumentos como estruturas de dados (normalmente listas). Ou seja, provavelmente a chamada recursiva (que é passada como argumento para a macro lazy-seq
) não é avaliada até que a primeira execução de even-numbers
já tenha retornado.
Ahá!
Mas… alguma coisa ainda não batia. Eu não entendia completamente como a sequência infinita estava sendo gerada. Se a chamada recursiva não era executada durante a chamada atual de even-numbers
, o que exatamente a função cons
estava retornando no trecho de código a seguir?
(cons n (lazy-seq (even-numbers (+ n 2)))))
Eu sabia que lazy-seq
retorna uma sequência lazy, cujos elementos não são “realizados” (computados) até que algum código cliente tente acessá-los. Ou seja, a chamada recursiva de even-numbers
em:
(cons n (lazy-seq (even-numbers (+ n 2)))))
seria executada somente quando outro código tentasse acessar pela primeira vez os valores da lazy sequence retornada por lazy-seq
.
Ora, mas não é isso que a função cons
está fazendo?? No meu entendimento, para que o código (cons n (lazy-seq …))
pudesse ser executado, seria necessário que a lazy sequence fosse completamente realizada, para só então “juntar” com o elemento n
!
E se fosse assim, a realização da lazy sequence deveria acarretar na chamada recursiva de even-numbers
, o que eventualmente causaria o stack overflow…
Segunda pista: cons não força a realização da Lazy Sequence!
Depois de gastar algum tempo pesquisando no Google e me deparar com essa ótima resposta de Alex D no StackOverflow (ironicamente), eu finalmente compreendi: Acontece que cons
NÃO FORÇA a realização da lazy sequence!
Isso não acontece por exemplo com a funçãoconj
. Se reescrevermos a função even-numbers
utilizando conj
ao invés de cons
, ocorrerá o stack overflow como esperado:
(defn even-numbers
([] (even-numbers 0))
([n] (conj (lazy-seq (even-numbers (+ n 2))) n )))(take 10 (even-numbers))
; => StackOverflowError
Os detalhes do porquê de cons
não forçar a realização da sequência estão explicados na resposta do link. Em resumo, tem a ver com o fato de cons
retornar objetos da classe clojure.lang.Cons
;
Os objetos dessa classe representam sequências (implementam a interface clojure.lang.ISeq
) e são estruturados internamente com duas referências: um objeto “_first” (o primeiro item da sequência) e um objeto “_more” (o restante da sequência, que é também uma sequência). O objeto Cons
não precisa saber se seu “_more” consiste em uma lista, um vetor ou mesmo uma Lazy Sequence; Tudo o que importa é que seja um objeto que também implemente ISeq
. Além disso, o objeto Cons
não precisa saber nada sobre os elementos da sequência “_more”. Portanto, ele não precisa realizar a sequência (ao contrário de conj
, que precisa realizar a sequência para completar sua execução).
Conclusão
Retornando à função even-numbers
:
(defn even-numbers
([] (even-numbers 0))
([n] (cons n (lazy-seq (even-numbers (+ n 2))))))(take 10 (even-numbers))
Quando essa função é invocada, os seguintes passos são executados:
- Primeiramente, a macro
lazy-seq
cria e retorna um objeto do tipoclojure.lang.LazySeq
. Esse objeto representa uma lazy sequence que ainda não possui nenhum elemento realizado (computado), mas que vai executar o código(even-numbers (+ n 2))
para obter uma sequência real assim que seus elementos forem requisitados. - Em seguida, a função
cons
cria um objeto do tipoclojure.lang.Cons
, seta sua propriedade “_first” para o valorn
e sua propriedade “_more” para o objetoLazySeq
retornado anteriormente porlazy-seq
. Até o momento, o objetoLazySeq
não precisou realizar nenhum elemento da sequência. - A função
even-numbers
retorna o objetoCons
. - A função
take
tenta acessar o primeiro elemento da sequência, e o objetoCons
retorna o seu “_first” (o valorn = 0
). - A função
take
tenta acessar o segundo elemento da sequência, e o objetoCons
delega o acesso a esse elemento para o seu “_more”, que é a lazy sequence. O objetoLazySeq
executa o código que foi passado para a macro:(even-numbers (+ n 2))
. O retorno desse código é um novo objetoCons
, cujo “_first” é o valor 2 e cujo “_more” é uma nova lazy sequence. O valor 2 é retornado para a funçãotake
. - O processo se repete.
Portanto, esse método de construção de sequências infinitas em Clojure é possível não apenas por causa da macro lazy-seq
mas também pelo comportamento da função cons
que representa uma sequência em termos de um elemento “_first” e outra sub-sequência “_more”.