Y combinator y optimizaciones de cola

20

La definición de un combinador Y en F # es

let rec y f x = f (y f) x

f espera tener como primer argumento alguna continuación para los subproblemas recursivos. Usando yf como continuación, vemos que f se aplicará a llamadas sucesivas a medida que podamos desarrollar

let y f x = f (y f) x = f (f (y f)) x = f (f (f (y f))) x etc...

El problema es que, a priori, este esquema impide el uso de cualquier optimización de llamada de cola: de hecho, podría haber alguna operación pendiente en las f, en cuyo caso no podemos simplemente mutar el marco de pila local asociado con f.

Entonces :

  • por un lado, usar el combinador Y requiere una continuación explícitamente diferente de la función misma.
  • Por otro lado, para aplicar el TCO, nos gustaría no tener ninguna operación pendiente en f y solo llamar a f.

¿Conoces alguna forma en que esos dos puedan reconciliarse? ¿Como una Y con truco de acumulador, o una Y con truco de CPS? ¿O un argumento que demuestre que no hay forma de hacerlo?

nicolas
fuente
¿Has agregado el trabajo de grabación rec a tu implementación? Creo que lo necesita de mi lectura ..
Jimmy Hoffa
¿Tiene pruebas de que no optimiza la llamada de cola? Creo que es posible que desee leer el IL para esa función y ver, no me sorprendería si el compilador es lo suficientemente inteligente como para llegar a algo ..
Jimmy Hoffa
en el caso de una recursión directa no vinculada, no lo hace; sin embargo, puede reescribirla para permitir que tal cosa esté sujeta al hecho de que el marco de la pila se reutiliza a través de la llamada y. Sí, podría necesitar ver la IL, no hay experiencia en eso.
nicolas
55
Hice una cuenta y obtuve 50 puntos solo para comentar aquí. Esta pregunta es realmente interesante. Creo que depende completamente de f. Podemos ver que ypodría hacer funa llamada final con un sonido (y f), pero como usted dice, fpodría tener alguna operación pendiente. Creo que sería interesante saber si hay un combinador separado que sea más amigable con la cola. Me pregunto si esta pregunta recibiría mejor atención en el sitio CS Stackexchange.
John Cartwright

Respuestas:

4

¿Conoces alguna forma en que esos dos puedan reconciliarse?

No, y con razón, en mi humilde opinión.

El Y-combinator es una construcción teórica y solo es necesaria para completar el cálculo lambda (recuerde, no hay bucles en el cálculo lambda, ni las lambdas tienen nombres que podamos usar para la recursión).

Como tal, el combinador Y es realmente fascinante.

Pero : ¡en realidad nadie usa el combinador Y para la recursión real! (Excepto tal vez por diversión, para demostrar que realmente funciona).

La optimización de llamadas de cola, OTOH, es, como su nombre lo indica, una optimización. No agrega nada a la expresividad de un lenguaje, solo nos preocupamos por consideraciones prácticas como el espacio de pila y el rendimiento del código recursivo.

Entonces su pregunta es: ¿Hay soporte de hardware para la reducción beta? (La reducción beta es cómo se reducen las expresiones lambda, ya sabes). Pero ningún lenguaje funcional (que yo sepa) compila su código fuente a una representación de expresiones lambda que se reducirán beta en tiempo de ejecución.

Ingo
fuente
2
El combinador en Y es como volver a escribir un nudo que se desata después de cada uso. La mayoría de los sistemas acortan esto y atan el nudo en el meta-nivel para que nunca necesite ser reintentado.
Dan D.
1
En cuanto al último párrafo, considere a Haskell, que en esencia utiliza la reducción de gráficos para hacer una evaluación perezosa. Pero mi favorito es la reducción óptima que siempre toma el camino en la red de Church-Rosser con las menores reducciones a la forma normal completa. Tal como aparece en La implementación óptima de los lenguajes de programación funcional de Asperti y Guerrini . Ver también BOHM 1.1 .
Dan D.
@DanD. Gracias por los enlaces, los probaré más tarde en un navegador compatible con postscript. Claro que hay algo que aprender para mí. Pero, ¿estás seguro de que Haskell compilado hace reducción de gráficos? Dudo esto
Ingo
1
Realmente utiliza la reducción de gráficos: "GHC compila a la máquina G sin etiquetas (STG) sin espinas. Esta es una máquina de reducción de gráficos nocional (es decir, una máquina virtual que realiza reducciones de gráficos como se describió anteriormente)". De ... Para más información sobre la máquina STG, vea Implementación de lenguajes funcionales perezosos de Simon Peyton Jones en el hardware de stock: la máquina G sin etiqueta de Spineless .
Dan D.
@DanD. en el mismo artículo que vinculó, se lee más abajo que GHC "hace una serie de optimizaciones en esa representación, antes de finalmente compilarlo en código de máquina real (posiblemente a través de C usando GCC)".
Ingo
0

No estoy completamente seguro de esta respuesta, pero es lo mejor que se me ocurrió.

El combinador y es inherentemente perezoso, en lenguajes estrictos, la pereza debe agregarse manualmente a través de lambdas adicionales.

let rec y f x = f (y f) x

Parece que su definición requiere pereza para terminar, o el (y f)argumento nunca terminaría de evaluar y tendría que evaluar si lo fusó o no . El TOC en un contexto vago es más complicado, y además el resultado de la (y f)composición de funciones repetidas sin aplicación con el x. No estoy seguro de que esto necesite tomar O (n) memoria donde n es la profundidad de la recursión, pero dudo que pueda lograr el mismo tipo de TOC que sea posible con algo como (cambiar a Haskell porque no sé realmente F#)

length acc []    = acc
length acc (a:b) = length (acc+1) b 

Si aún no lo sabe, la diferencia entre foldly foldl'en Haskell puede arrojar algo de luz sobre la situación. foldlestá escrito como se haría en un idioma entusiasta. Pero en lugar de ser TOC, en realidad es peor que foldrporque el acculador almacena una potencia potencialmente enorme que no puede evaluarse parcialmente. (Esto está relacionado con por qué tanto foldl como foldl 'no funcionan en listas infinitas.) Por lo tanto, en versiones más recientes de Haskell, foldl'se agregó lo que obliga a evaluar el acumulador cada vez que la función se repite para garantizar que no se cree un enorme thunk. Estoy seguro de que http://www.haskell.org/haskellwiki/Foldr_Foldl_Foldl%27 puede explicar esto mejor que yo.

Ericson2314
fuente