Polimorfismo de rango superior sobre tipos sin caja


10

Tengo un idioma en el que los tipos no están en caja de forma predeterminada, con la inferencia de tipos basada en Hindley-Milner. Me gustaría agregar un polimorfismo de rango superior, principalmente para trabajar con tipos existenciales.

Creo que entiendo cómo verificar estos tipos, pero no estoy seguro de qué hacer al compilar. Actualmente, compilo definiciones polimórficas generando especializaciones, muy parecidas a las plantillas de C ++, para que puedan trabajar con valores sin caja. Por ejemplo, dada una definición de f<T>, si el programa solo invoca f<Int32>y f<Char>, entonces, solo esas especializaciones aparecen en el programa compilado. (Estoy asumiendo la compilación de todo el programa por ahora).

Pero cuando paso una función polimórfica como argumento, no veo cómo puedo generar la especialización correcta estáticamente, porque la función podría seleccionarse en tiempo de ejecución. ¿No tengo más remedio que usar una representación en recuadro? ¿O hay alguna forma de evitar el problema?

Mi primer pensamiento fue codificar de alguna manera el polimorfismo de rango n como rango 1, pero no creo que sea posible en general porque una fórmula en lógica constructiva no necesariamente tiene una forma normal prenexa.


Una alternativa es reducir la cantidad de boxeo necesaria almacenando mapas de bits para los cuales los argumentos de una función y las palabras en la memoria son punteros. Entonces, una función / estructura polimórfica es realmente polimórfica sobre un puntero o una palabra de datos arbitraria, y las estructuras pueden almacenar su último campo (incluso si es polimórfico) en línea. El GC también puede usar esos mapas de bits para evitar la necesidad de palabras de etiqueta para tipos que no sean de suma.
fread2281

@ fread2281: en realidad solía hacer algo así en una versión anterior del lenguaje. Actualmente no genero etiquetas para tipos que no son de suma, y ​​no hay GC. Creo que eso también es compatible con el enfoque de Neel K.
Jon Purdy

Respuestas:


6

He pensado un poco en esto. El problema principal es que, en general, no sabemos qué tan grande es un valor de tipo polimórfico. Si no tiene esta información, debe obtenerla de alguna manera. La monomorfización obtiene esta información para usted al especializar el polimorfismo. El boxeo obtiene esta información para usted al poner todo en una representación de tamaño conocido.

Una tercera alternativa es hacer un seguimiento de esta información en los tipos. Básicamente, lo que puede hacer es introducir un tipo diferente para cada tamaño de datos, y luego las funciones polimórficas se pueden definir sobre todos los tipos de un tamaño particular. Dibujaré un sistema de este tipo a continuación.

Tiposκ:: =norteConstructores tipoUNA:: =una:κ.UNAEl |αEl |UNA×siEl |UNA+siEl |UNAsiEl |rmiFUNAEl |PAGunare(k)El |μα:κ.UNA

Aquí, la idea de alto nivel es que el tipo de tipo te dice cuántas palabras se necesitan para colocar un objeto en la memoria. Para cualquier tamaño dado, es fácil ser polimórfico sobre todos los tipos de ese tamaño en particular. Dado que cada tipo, incluso los polimórficos, todavía tiene un tamaño conocido, la compilación no es más difícil de lo que es para C.

Las reglas de clasificación convierten este inglés en matemáticas, y deberían verse así:

α:norteΓΓα:norteΓ,α:norteUNA:metroΓα:norte.UNA:metro
ΓUNA:norteΓsi:metroΓUNA×si:norte+metroΓUNA:norteΓsi:norteΓUNA+si:norte+1
ΓUNA:metroΓsi:norteΓUNAsi:1ΓUNA:norteΓrmiFUNA:1
ΓPAGunare(k):kΓ,α:norteUNA:norteΓμα:norte.UNA:norte

Por lo tanto, el cuantificador general requiere que proporcione el tipo que está buscando. Del mismo modo, el emparejamiento es un tipo de par sin caja, que simplemente presenta una junto a una en la memoria (como un tipo de estructura C). Las uniones disjuntas toman dos valores del mismo tamaño y luego agregan una palabra para una etiqueta discriminadora. Las funciones son cierres, representados como de costumbre por un puntero a un registro del entorno y el código.UNA×siUNAsi

Las referencias son interesantes: los punteros son siempre una palabra, pero pueden apuntar a valores de cualquier tamaño. Esto permite a los programadores implementar el polimorfismo en objetos arbitrarios mediante el boxeo, pero no requiere que lo hagan. Finalmente, una vez que los tamaños explícitos están en juego, a menudo es útil introducir un tipo de relleno, que usa espacio pero no hace nada. (Entonces, si desea tomar la unión disjunta de un int y un par de entradas, necesitará agregar relleno al primer int, para que el diseño del objeto sea uniforme).

Los tipos recursivos tienen la regla de formación estándar, pero tenga en cuenta que las ocurrencias recursivas tienen que ser del mismo tamaño, lo que significa que generalmente tiene que pegarlas en un puntero para que la clasificación funcione. Por ejemplo, el tipo de datos de la lista podría representarse como

μα:1)rmiF(PAGunare(2)+yonortet×α)

Entonces, esto apunta a un valor de lista vacío, o un par de un int y un puntero a otra lista vinculada.

La verificación de tipos para sistemas como este tampoco es muy difícil; el algoritmo en mi documento ICFP con Joshua Dunfield, la verificación de tipo bidireccional completa y fácil para el polimorfismo de rango superior se aplica a este caso casi sin cambios.


Genial, creo que esto cubre mi caso de uso. Era consciente de usar tipos para razonar sobre representaciones de valores (como GHC *vs. #), pero no había considerado hacerlo de esta manera. Parece razonable restringir los cuantificadores de mayor rango a tipos de tamaño conocido, y creo que esto también me permitiría generar estáticamente especializaciones por tamaño, sin necesidad de conocer el tipo real. Ahora, es hora de volver a leer ese documento. :)
Jon Purdy

1

Esto parece estar más cerca de un problema de compilación que de un problema de "informática teórica", por lo que probablemente sea mejor preguntar en otro lado.

En el caso general, de hecho, creo que no hay otra solución que usar una representación en recuadro. Pero también espero que en la práctica haya muchas opciones alternativas diferentes, dependiendo de los detalles de su situación.

Por ejemplo, la representación de bajo nivel de argumentos sin caja generalmente se puede clasificar en muy pocas alternativas, por ejemplo, entero o similar, punto flotante o puntero. Entonces, para una función f<T>, tal vez solo necesite generar 3 implementaciones diferentes sin caja y pueda representar la polimórfica como una tupla de esas 3 funciones, por lo que instanciar T a Int32 es solo seleccionar el primer elemento de la tupla, ...


Gracias por tu ayuda. No estaba realmente seguro de dónde preguntar, ya que un compilador abarca desde la teoría de alto nivel hasta la ingeniería de bajo nivel, pero pensé que la gente de por aquí tendría algunas ideas. Parece que el boxeo puede ser el enfoque más flexible aquí. Después de leer su respuesta y pensarlo más, la única otra solución razonable que he podido encontrar es renunciar a cierta flexibilidad y requerir que los argumentos polimórficos se conozcan estáticamente, por ejemplo, pasándolos como parámetros de tipo. Son compensaciones hasta el final. : P
Jon Purdy

44
La pregunta del OP contiene problemas de TCS perfectamente válidos, como cómo hacer inferencia de tipos cuando Damas-Hindley-Milner se extiende con tipos de rango superior. En general, el polimorfismo de rango 2 tiene una inferencia de tipo decidible, pero para el rango k> 2, la inferencia de tipo es indecidible. Si la restricción Damas-Hindley-Milner cambia esto, no lo sé. Finalmente, casi todo lo que hacen los compiladores modernos debería ser parte de TCS, pero generalmente no lo es porque los implementadores del compilador están por delante de los teóricos.
Martin Berger
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.