La cuestión es que realmente no hay mucho margen de maniobra en términos de codificación de funciones. Estas son las principales opciones:
Reescritura de términos: almacena funciones como sus árboles de sintaxis abstracta (o alguna codificación de la misma. Cuando llama a una función, atraviesa manualmente el árbol de sintaxis para reemplazar sus parámetros con el argumento. Esto es fácil, pero terriblemente ineficiente en términos de tiempo y espacio .
Cierres: tiene alguna forma de representar una función, tal vez un árbol de sintaxis, más probablemente un código de máquina. Y en estas funciones, se refiere a sus argumentos por referencia de alguna manera. Podría ser un desplazamiento de puntero, podría ser un número entero o un índice De Bruijn, podría ser un nombre. Luego representa una función como un cierre : las "instrucciones" de la función (árbol, código, etc.) emparejadas con una estructura de datos que contiene todas las variables libres de la función. Cuando una función se aplica realmente, de alguna manera sabe cómo buscar las variables libres en su estructura de datos, utilizando entornos, aritmética de punteros, etc.
Estoy seguro de que hay otras opciones, pero estas son las básicas, y sospecho que casi cualquier otra opción será una variante u optimización de la estructura de cierre básica.
Por lo tanto, en términos de rendimiento, los cierres funcionan casi universalmente mejor que la reescritura de términos. De las variaciones, ¿cuál es mejor? Eso depende en gran medida de su lenguaje y arquitectura, pero sospecho que el "código de máquina con una estructura que contiene vars libres" es el más eficiente. Tiene todo lo que la función necesita (instrucciones y valores) y nada más, y la llamada no termina haciendo recorridos a largo plazo.
Estoy interesado tanto en el uso del algoritmo de codificación actual de los lenguajes funcionales populares (Haskell, ML)
No soy un experto, pero soy el 99% de la mayoría de los sabores de ML que usan alguna variación de los cierres que describo, aunque con algunas optimizaciones probables. Vea esto para una perspectiva (posiblemente desactualizada).
Haskell hace algo un poco más complicado debido a la evaluación perezosa: utiliza la reescritura de gráficos sin etiquetas de Spineless .
y también en el más eficiente que se pueda lograr.
¿Qué es lo más eficiente? No hay una implementación que sea más eficiente en todas las entradas, por lo que obtendrá implementaciones que son eficientes en promedio, pero cada una sobresaldrá en diferentes escenarios. Por lo tanto, no hay una clasificación definitiva de los más o menos eficientes.
No hay magia aquí. Para almacenar una función, necesita almacenar sus valores libres de alguna manera, de lo contrario está codificando menos información que la función en sí. Tal vez pueda optimizar algunos de los valores libres con una evaluación parcial pero eso es arriesgado para el rendimiento, y debe tener cuidado para asegurarse de que esto siempre se detenga.
Y, tal vez pueda usar algún tipo de compresión o algoritmo inteligente para ganar eficiencia en el espacio. Pero entonces, o bien está intercambiando tiempo por espacio, o se encuentra en una situación en la que se optimizó para algunos casos y disminuyó la velocidad para otros.
Se puede optimizar para el caso común, pero lo que el caso común es puede cambiar el idioma, área de aplicación, etc. El tipo de código que es rápido para un videojuego (cálculo de números, lazos estrechos con gran entrada) es probablemente diferente de qué es rápido para un compilador (recorridos en árbol, listas de trabajo, etc.).
Punto de bonificación: ¿Existe tal codificación que asigne los enteros codificados de funciones a enteros nativos (short, int, etc. en C). ¿Es posible?
No, esto no es posible. El problema es que el cálculo lambda no te permite introspectar términos. Cuando una función toma un argumento con el mismo tipo que un número de Iglesia, necesita poder llamarlo, sin examinar la definición exacta de ese número. Eso es lo que sucede con las codificaciones de la Iglesia: lo único que puedes hacer con ellas es llamarlas, y puedes simular todo lo útil con esto, pero no sin costo.
Más importante aún, los enteros ocupan cada codificación binaria posible. Entonces, si las lambdas se representaran como sus enteros, ¡no tendrías forma de representar lambdas no eclesiásticas! O bien, introduciría una bandera para indicar si una lambda es un número o no, pero entonces cualquier eficiencia que desee probablemente se haya ido por la ventana.
EDITAR: desde que escribí esto, me he dado cuenta de una tercera opción para implementar funciones de orden superior: la desfuncionalización . Aquí, cada llamada a la función se convierte en una gran switch
declaración, dependiendo de qué abstracción lambda se haya dado como función. La desventaja aquí es que es una transformación completa del programa: no puede compilar partes por separado y luego vincularlas de esta manera, ya que necesita tener el conjunto completo de abstracciones lambda con anticipación.