He estado haciendo algunos JavaScript funcionales. Pensé que se había implementado Tail-Call Optimization , pero resultó que estaba equivocado. Por lo tanto, tuve que enseñarme el trampolín . Después de leer un poco aquí y en otros lugares, pude obtener lo básico y construí mi primer trampolín:
/*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)));
Mi mayor problema es que no sé por qué esto funciona. Tengo la idea de volver a ejecutar la función en un ciclo while en lugar de usar un ciclo recursivo. Excepto, técnicamente mi función base ya tiene un bucle recursivo. No estoy ejecutando la loopy
función base , pero estoy ejecutando la función dentro de ella. ¿Qué impide que se foo = foo()
produzca un desbordamiento de pila? ¿Y no está foo = foo()
técnicamente mutando, o me estoy perdiendo algo? Quizás es solo un mal necesario. O alguna sintaxis que me falta.
¿Hay alguna manera de entenderlo? ¿O es solo un truco que de alguna manera funciona? He podido abrirme camino a través de todo lo demás, pero este me tiene confundido.
loopy
no se desborda porque no se llama a sí mismo .Respuestas:
La razón por la cual su cerebro se está rebelando contra la función
loopy()
es que es de un tipo inconsistente :Muchos idiomas ni siquiera te permiten hacer cosas como esta, o al menos exigen mucho más tipeo para explicar cómo se supone que esto tiene algún tipo de sentido. Porque realmente no lo hace. Las funciones y los enteros son tipos de objetos totalmente diferentes.
Así que veamos ese ciclo while, con cuidado:
Inicialmente,
foo
es igual aloopy(0)
. ¿Qué esloopy(0)
? Bueno, es menos de 10000000, así que lo tenemosfunction(){return loopy(1)}
. Ese es un valor verdadero, y es una función, por lo que el ciclo continúa.Ahora llegamos a
foo = foo()
.foo()
es el mismo queloopy(1)
. Como 1 todavía es inferior a 10000000, eso devuelvefunction(){return loopy(2)}
, que luego asignamosfoo
.foo
sigue siendo una función, así que seguimos adelante ... hasta que eventualmente foo es igual afunction(){return loopy(10000000)}
. Esa es una función, así que lo hacemosfoo = foo()
una vez más, pero esta vez, cuando llamamosloopy(10000000)
, x no es inferior a 10000000, así que solo recuperamos x. Como 10000000 tampoco es una función, esto también finaliza el ciclo while.fuente
Kevin señala sucintamente cómo funciona este fragmento de código en particular (junto con por qué es bastante incomprensible), pero quería agregar información sobre cómo funcionan los trampolines en general .
Sin la optimización de llamada de cola (TCO), cada llamada de función agrega un marco de pila a la pila de ejecución actual. Supongamos que tenemos una función para imprimir una cuenta regresiva de números:
Si llamamos
countdown(3)
, analicemos cómo se vería la pila de llamadas sin TCO.Con TCO, cada llamada recursiva a
countdown
está en posición de cola (no queda nada más que hacer que devolver el resultado de la llamada), por lo que no se asigna ningún marco de pila. Sin TCO, la pila explota incluso un poco más granden
.El trampolín evita esta restricción insertando un contenedor alrededor de la
countdown
función. Luego,countdown
no realiza llamadas recursivas y en su lugar, inmediatamente devuelve una función para llamar. Aquí hay un ejemplo de implementación:Para tener una mejor idea de cómo funciona esto, veamos la pila de llamadas:
En cada paso, la
countdownHop
función abandona el control directo de lo que sucede a continuación, en cambio devuelve una función para llamar que describe lo que le gustaría que suceda a continuación. La función de trampolín toma esto y lo llama, luego llama a cualquier función que regrese, y así sucesivamente hasta que no haya un "siguiente paso". Esto se llama trampolín porque el flujo de control "rebota" entre cada llamada recursiva y la implementación del trampolín, en lugar de que la función se repita directamente. Al abandonar el control sobre quién hace la llamada recursiva, la función de trampolín puede garantizar que la pila no sea demasiado grande. Nota al margen: esta implementación detrampoline
omite valores de retorno por simplicidad.Puede ser complicado saber si es una buena idea. El rendimiento puede verse afectado debido a que cada paso asigna un nuevo cierre. Las optimizaciones inteligentes pueden hacer que esto sea viable, pero nunca se sabe. El trampolín es principalmente útil para sortear los límites de recursión, por ejemplo, cuando una implementación de lenguaje establece un tamaño máximo de pila de llamadas.
fuente
Quizás sea más fácil de entender si el trampolín se implementa con un tipo de retorno dedicado (en lugar de abusar de una función):
Compare esto con su versión de
trampoline
, donde el caso de recursión es cuando la función devuelve otra función, y el caso base es cuando devuelve algo más.Ya no se llama a sí mismo. En cambio, devuelve un resultado (en mi implementación, literalmente a
Result
) que transmite si continuar la recursión o si estallar.Sí, este es exactamente el mal necesario del bucle.
trampoline
También se podría escribir sin mutación, pero requeriría recurrencia nuevamente:Aún así, muestra la idea de lo que la función de trampolín hace aún mejor.
El objetivo del trampoling es abstraer la llamada recursiva de la cola de la función que quiere usar la recursión en un valor de retorno, y hacer la recursión real en un solo lugar: la
trampoline
función, que luego se puede optimizar en un solo lugar para usar un lazo.fuente
foo = foo()
es una mutación en el sentido de modificar el estado local, pero generalmente consideraría esa reasignación ya que en realidad no está modificando el objeto de función subyacente, lo está reemplazando con la función (o valor) que devuelve.foo
contiene, solo se modifica la variable. Unwhile
bucle requiere un estado mutable si desea que finalice, en este caso la variablefoo
ox
.fn
en una llamada recursiva atrampoline
: no estoy seguro de que sea una mejora.