Eu tenho feito algum JavaScript funcional. Eu pensava que a Otimização de chamada de cauda havia sido implementada, mas, como se vê, eu estava errado. Assim, eu tive que me ensinar trampolim . Depois de ler um pouco aqui e em outros lugares, consegui entender o básico e construí meu primeiro trampolim:
/*not the fanciest, it's just meant to
reenforce that I know what I'm doing.*/
function loopy(x){
if (x<10000000){
return function(){
return loopy(x+1)
}
}else{
return x;
}
};
function trampoline(foo){
while(foo && typeof foo === 'function'){
foo = foo();
}
return foo;
/*I've seen trampolines without this,
mine wouldn't return anything unless
I had it though. Just goes to show I
only half know what I'm doing.*/
};
alert(trampoline(loopy(0)));
Meu maior problema é que não sei por que isso funciona. Tenho a ideia de executar novamente a função em um loop while, em vez de usar um loop recursivo. Exceto que tecnicamente minha função base já possui um loop recursivo. Não estou executando a loopy
função base , mas estou executando a função dentro dela. O que está impedindo foo = foo()
de causar um estouro de pilha? E não é foo = foo()
tecnicamente mutante, ou estou faltando alguma coisa? Talvez seja apenas um mal necessário. Ou falta alguma sintaxe.
Existe alguma maneira de entender isso? Ou é apenas algum truque que de alguma forma funciona? Eu fui capaz de fazer o meu caminho através de todo o resto, mas este me deixou perplexo.
loopy
não transborda porque não se chama .Respostas:
A razão pela qual seu cérebro está se rebelando contra a função
loopy()
é que é de um tipo inconsistente :Muitos idiomas nem permitem que você faça coisas assim, ou pelo menos exigem muito mais digitação para explicar como isso deve fazer algum sentido. Porque realmente não. Funções e números inteiros são tipos totalmente diferentes de objetos.
Então, vamos passar por esse loop while, com cuidado:
Inicialmente,
foo
é igual aloopy(0)
. O que éloopy(0)
? Bem, é menos que 10000000, então chegamosfunction(){return loopy(1)}
. Esse é um valor verdadeiro, e é uma função, para que o loop continue.Agora chegamos a
foo = foo()
.foo()
é o mesmo queloopy(1)
. Como 1 ainda é menor que 10000000, isso retornafunction(){return loopy(2)}
, ao qual atribuímosfoo
.foo
ainda é uma função, então continuamos ... até que foo seja igual afunction(){return loopy(10000000)}
. Essa é uma função, então fazemosfoo = foo()
mais uma vez, mas desta vez, quando chamamosloopy(10000000)
, x não é inferior a 10000000, portanto, apenas recebemos x de volta. Como 10000000 também não é uma função, isso encerra o loop while.fonte
Kevin aponta sucintamente como esse trecho de código específico funciona (junto com o motivo pelo qual é bastante incompreensível), mas eu queria adicionar algumas informações sobre como os trampolins em geral funcionam.
Sem otimização de chamada de cauda (TCO), toda chamada de função adiciona um quadro de pilha à pilha de execução atual. Suponha que tenhamos uma função para imprimir uma contagem regressiva de números:
Se ligarmos
countdown(3)
, vamos analisar como ficaria a pilha de chamadas sem o TCO.Com o TCO, cada chamada recursiva para
countdown
está na posição final (não há mais nada a fazer além de retornar o resultado da chamada), portanto, nenhum quadro de pilha é alocado. Sem o TCO, a pilha explode para um pouco maiorn
.O trampolim contorna essa restrição inserindo um invólucro em torno da
countdown
função. Em seguida,countdown
não realiza chamadas recursivas e retorna imediatamente uma função para chamar. Aqui está um exemplo de implementação:Para ter uma noção melhor de como isso funciona, vejamos a pilha de chamadas:
Em cada etapa, a
countdownHop
função abandona o controle direto do que acontece a seguir, em vez disso, retorna uma função para chamar que descreve o que gostaria que acontecesse a seguir. A função trampolim então pega e chama, então chama qualquer função que retorna, e assim por diante até que não haja "próximo passo". Isso é chamado de trampolim porque o fluxo de controle "salta" entre cada chamada recursiva e a implementação do trampolim, em vez da função diretamente recorrente. Ao abandonar o controle sobre quem faz a chamada recursiva, a função trampolim pode garantir que a pilha não fique muito grande. Nota lateral: esta implementaçãotrampoline
omite os valores retornados por simplicidade.Pode ser complicado saber se essa é uma boa ideia. O desempenho pode sofrer devido a cada etapa que aloca um novo fechamento. Otimizações inteligentes podem tornar isso viável, mas você nunca sabe. O trampolim é útil principalmente para contornar limites rígidos de recursão, por exemplo, quando uma implementação de idioma define um tamanho máximo de pilha de chamadas.
fonte
Talvez seja mais fácil entender se o trampolim é implementado com um tipo de retorno dedicado (em vez de abusar de uma função):
Compare isso com a sua versão de
trampoline
, onde o caso de recursão é quando a função retorna outra função, e o caso base é quando retorna outra coisa.Não se chama mais. Em vez disso, ele retorna um resultado (na minha implementação, literalmente a
Result
) que indica se deve continuar a recursão ou se deve ocorrer .Sim, este é exatamente o mal necessário do loop. Pode-se escrever
trampoline
sem mutação também, mas exigiria recursão novamente:Ainda assim, mostra a idéia do que a função trampolim faz ainda melhor.
O objetivo do trampolim é abstrair a chamada recursiva da função que deseja usar a recursão em um valor de retorno e fazer a recursão real em apenas um local - a
trampoline
função, que pode ser otimizada em um único local para usar um ciclo.fonte
foo = foo()
é uma mutação no sentido de modificar o estado local, mas geralmente considero que a reatribuição como você não está realmente modificando o objeto de função subjacente, está substituindo-o pela função (ou valor) que ele retorna.foo
contém, apenas a variável é modificada. Umwhile
loop requer algum estado mutável se você deseja finalizar, neste caso a variávelfoo
oux
.fn
em uma chamada recursiva paratrampoline
- não tenho certeza se isso é uma melhoria.