recursivos recursivas potencia funciones ejercicios ejemplos ciclos haskell frp

haskell - recursivas - ¿Por qué recursivo `let` hace que el espacio sea efciente?



funciones recursivas ejemplos (3)

En pocas palabras, las variables se comparten, pero las aplicaciones de funciones no lo son. En

repeat x = x : repeat x

es una coincidencia (desde la perspectiva del lenguaje) que la llamada (co) recursiva a repetir sea con el mismo argumento. Entonces, sin una optimización adicional (que se llama transformación de argumento estático), la función se llamará una y otra vez.

Pero cuando escribes

repeat x = let xs = x : xs in xs

no hay llamadas a funciones recursivas. Tomas una x y construyes un valor xs cíclico usándola. Todo el intercambio es explícito.

Si quieres entenderlo de manera más formal, debes familiarizarte con la semántica de la evaluación perezosa, como A Natural Semantics for Lazy Evaluation .

Encontré esta afirmación mientras estudiaba la Programación reactiva funcional, de "Tapar una fuga de espacio con una flecha" por Hai Liu y Paul Hudak (página 5):

Suppose we wish to define a function that repeats its argument indefinitely: repeat x = x : repeat x or, in lambdas: repeat = λx → x : repeat x This requires O(n) space. But we can achieve O(1) space by writing instead: repeat = λx → let xs = x : xs in xs

La diferencia aquí parece pequeña pero impulsa enormemente la eficiencia del espacio. ¿Por qué y cómo sucede? La mejor conjetura que he hecho es evaluarlos a mano:

r = /x -> x: r x r 3 -> 3: r 3 -> 3: 3: 3: ........ -> [3,3,3,......]

Como se indicó anteriormente, tendremos que crear infinitos nuevos thunk para estas recursiones. Luego trato de evaluar el segundo:

r = /x -> let xs = x:xs in xs r 3 -> let xs = 3:xs in xs -> xs, according to the definition above: -> 3:xs, where xs = 3:xs -> 3:xs:xs, where xs = 3:xs

En la segunda forma, aparece el xs y se puede compartir entre cada lugar que ocurre, así que supongo que es por eso que solo podemos requerir O(1) espacios en lugar de O(n) . Pero no estoy seguro de si estoy en lo cierto o no.

Por cierto: la palabra clave "compartido" proviene de la misma página de papel 4:

El problema aquí es que las reglas estándar de evaluación de llamada por necesidad no pueden reconocer que la función:

f = λdt → integralC (1 + dt) (f dt)

es lo mismo que:

f = λdt → let x = integralC (1 + dt) x in x

La definición anterior hace que el trabajo se repita en la llamada recursiva a f, mientras que en el último caso el cálculo se comparte.


Es más fácil de entender con imágenes:

  • La primera versión

    repeat x = x : repeat x

    crea una cadena de (:) constructores que terminan en un thunk que se reemplazará con más constructores a medida que los exija. Por lo tanto, O (n) espacio.

  • La segunda versión

    repeat x = let xs = x : xs in xs

    usa let para "atar el nudo", creando un único (:) constructor que se refiere a sí mismo.


Tu intuición acerca de que xs se comparte es correcta. Para replantear el ejemplo del autor en términos de repetición, en lugar de integral, cuando escribe:

repeat x = x : repeat x

el lenguaje no reconoce que la repeat x a la derecha es la misma que el valor producido por la expresión x : repeat x . Mientras que si escribes

repeat x = let xs = x : xs in xs

está creando explícitamente una estructura que, cuando se evalúa, tiene el siguiente aspecto:

{hd: x, tl:|} ^ | /________/