Trampolim, exemplo em Java
Vamos fazer um programinha bem simples que soma os
números de n
até 0? Só que, no lugar de fazer ele
iterativo, que tal fazer recursivo?
Bem, vamos chamar esse programa de sum
. Sabemos que
sum(0) == 0
, então temos o caso base. E como chegamos
no caso base? Bem, sum(n) == n + sum(n-1)
, até que
eventualmente chegamos em sum(0)
. Ficaria assim em Java:
int sum(int n) {
if (n == 0) {
return 0;
}
return n + sum(n - 1);
}
Problema de recursão?
Bem, recursão tem um problema inerente quando o caso base está muito distante do valor passado… na maioria das linguagens, a chamada de função acaba fazendo uso da stack do programa para inserir os dados sobre a chamada da função, então eventualmente uma recursão muito grande poderá gerar um estouro de pilha.
Mas, será que tem como evitar isso? Na real, tem sim. E isso é uma estratégia as old as time. Se chama trampoline.
O trampolim
Basicamente a estratégia do trampoline é você fazer um pedaço do programa que retorna um “valor” ou uma “continuation”. O que seria a continuation? Uma função que irá continuar o processamento.
É mais ou menos isso daqui:
let trampolim = primeiraChamada(input);
while (trampolim is continuation) {
trampolim = trampolim.continue();
}
return trampolim;
O que seria a continuation do sum
?
Vamos modelar o programa sum
para, no lugar de ser
simplesmente recursivo, ter uma continuation. Bem, uma
maneira é passando o acc
como uma espécie de objeto
passado pela continuidade. Então, ao chegar em
sum_trampoline(0, acc)
retornemos acc
. E
passar para o passo seguinte? Bem…
Bora lá, saímos de sum_trampoline(n, acc)
para
sum_trampoline(n-1, acc+n)
. E a primeira entrada
é com sum_trampoline(n, 0)
.
Logo, ficaria assim o código:
Object sum_trampoline_bootstrap(int n) {
return sum_trampoline(n, 0);
}
Object sum_trampoline(int n, int acc) {
if (n == 0) {
return acc;
}
return (Supplier<Object>) () -> sum(n - 1, acc + n);
}
Descrevendo trampolim com tipos
O trampolim precisa ter mais ou menos a seguinte forma:
let trampolim = primeiraChamada(input);
while (trampolim is continuation) {
trampolim = trampolim.continue();
}
return trampolim;
Só que isso tem uma liberdade de codificação muito grande,
não é muito literal para o mundo Java. Podemos detectar que
é continuidade perguntando ao objeto. E que tal se perguntarmos
“já achou o valor?”? Outra coisa também é que, como em Java
não temos sum-types, o return trampolim
ali literalmente
retornaria o tipo trampolim
, no lugar de retornar o valor.
Podemos retornar trampolim.value()
.
Finalmente, temos um ponto crucial que é o bootstrapping do trampolim. Para isso, poderíamos ter uma função que transformasse o input no trampolim de retorno adequado. E input e resultado podem ser generalizados para melhor uso:
public static <IN, R> R trampoline(IN input,
Function<IN, TrampolineStep<R>> trampolinebootStrap) {
TrampolineStep<R> nextStep = trampolinebootStrap.apply(input);
while (!nextStep.gotValue()) {
nextStep = nextStep.runNextStep();
}
return nextStep.value();
}
E a interface de TrampolineStep<R>
?
De instância, ficaram definidos 3 métodos:
gotValue
, pergunta se já tem valorvalue
, retorna o valor encontradorunNextStep
, retorna um valor ou uma continuidade
Bem, basicamente ela vai ter dois estados:
- achou o valor
- é uma continuidade
Então podemos colocar com métodos estáticos para linicialização dela. Para o caso de já achou o valor, precisa-se passar o valor:
static <X> TrampolineStep<X> valueFound(X value) {
return new TrampolineStep<>() {
@Override
public boolean gotValue() {
return true;
}
@Override
public X value() {
return value;
}
@Override
public TrampolineStep<X> runNextStep() {
return this;
}
};
}
Para o caso de contnuidade, precisa passar como obter o próximo item da continuidade:
static <X> TrampolineStep<X> goonStep(Supplier<TrampolineStep<X>> x) {
return new TrampolineStep<>() {
@Override
public boolean gotValue() {
return false;
}
@Override
public X value() {
throw new RuntimeException("dont call this");
}
@Override
public TrampolineStep<X> runNextStep() {
return x.get();
}
};
}
Para o sum_trampoline
, como ele ficaria?
TrampolineStep<Integer> sum_trampoline_bootstrap(int n) {
return sum_trampoline(n, 0);
}
TrampolineStep<Integer> sum_trampoline(int n, int acc) {
if (n == 0) {
return TrampolineStep.valueFound(acc);
}
return TrampolineStep.goonStep(() -> sum_trampoline(n - 1, acc + n));
}
Tail call Fibonacci
A implementação clássica de Fibonacci segue a definição recursiva:
int fib(int n) {
if (n <= 1) {
return n;
}
return fib(n-1) + fib(n-2);
}
Existe ainda a versão iterativa, que desenrola a definição de Fibonacci não recursivamente, mas para a frente: começando de 0 e 1 e indo até o valor correspondente:
int fib_iterativo(int n) {
int a = 0, b = 1;
if (n == 0) {
return a;
}
int i = 1;
while (i < n) {
int x = a + b;
a = b;
b = x;
i++;
}
return b;
}
Essa implementação tem uma versão rolando para frente, usando o “tail call recursion”:
int fib_tc_entrada(int n) {
return fib_tc(0, 1, 0, n);
}
int fib_tc(int a, int b, int i, int n) {
if (i == n) {
return a;
}
return fib_tc(b, a+b, i+1, n);
}
Aqui separei na interface de entrada que prepara os dados
que serão usados no Fibonacci com tail call recursion. Como
ele avança para frente, começamos com os mapeamentos
fib[0] => 0
, fib[1] => 1
navegando a partir do índice
0
até chegar no índice n
.
Fibonacci, de tail call a trampoline
Bem, o exemplo de fib_tc
já dá uma ideia bem boa do que
seria o trampoline
para Fibonacci:
TrampolineStep<Integer> fib_bootstrap(int n) {
return fib_trampoline(0, 1, 0, n);
}
TrampolineStep<Integer> fib_trampoline(int a, int b, int i, int n) {
if (i == n) {
return TrampolineStep.valueFound(a);
}
return TrampolineStep.goonStep(() -> fib_trampoline(b, a+b, i+1, n));
}