No soy muy competente en Haskell, por lo que esta podría ser una pregunta muy fácil.
¿Qué limitación de idioma resuelve Rank2Types ? ¿Las funciones en Haskell ya no admiten argumentos polimórficos?
haskell
types
polymorphism
higher-rank-types
Andrey Shchekin
fuente
fuente

Respuestas:
Lo hacen, pero solo de rango 1. Esto significa que, si bien puede escribir una función que tome diferentes tipos de argumentos sin esta extensión, no puede escribir una función que use su argumento como diferentes tipos en la misma invocación.
Por ejemplo, la siguiente función no se puede escribir sin esta extensión porque
gse usa con diferentes tipos de argumentos en la definición def:Tenga en cuenta que es perfectamente posible pasar una función polimórfica como argumento a otra función. Entonces, algo así
map id ["a","b","c"]es perfectamente legal. Pero la función solo puede usarla como monomórfica. En el ejemplo semapusaidcomo si tuviera tipoString -> String. Y, por supuesto, también puede pasar una función monomórfica simple del tipo dado en lugar deid. Sin rank2types no hay forma de que una función requiera que su argumento sea una función polimórfica y, por lo tanto, tampoco hay forma de usarla como función polimórfica.fuente
f' g x y = g x + g y. Su tipo de rango 1 inferido esforall a r. Num r => (a -> r) -> a -> a -> r. Comoforall aestá fuera de las flechas de función, la persona que llama primero debe elegir un tipo paraa; si eligenInt, obtenemosf' :: forall r. Num r => (Int -> r) -> Int -> Int -> r, y ahora hemos arreglado elgargumento para que pueda tomarIntpero noString. Si lo habilitamosRankNTypespodemos anotarf'con typeforall b c r. Num r => (forall a. a -> r) -> b -> c -> r. Sin embargo, no puedo usarlo, ¿cuál seríag?Es difícil entender el polimorfismo de rango superior a menos que estudie el Sistema F directamente, porque Haskell está diseñado para ocultarle los detalles en aras de la simplicidad.
Pero básicamente, la idea aproximada es que los tipos polimórficos realmente no tienen la
a -> bforma que tienen en Haskell; en realidad, se ven así, siempre con cuantificadores explícitos:Si no conoce el símbolo "∀", se lee como "para todos";
∀x.dog(x)significa "para todo x, x es un perro". "Λ" es lambda mayúscula, utilizada para abstraer parámetros de tipo; lo que dice la segunda línea es que id es una función que toma un tipoty luego devuelve una función que está parametrizada por ese tipo.Verá, en el Sistema F, no puede simplemente aplicar una función como esa
ida un valor de inmediato; primero debe aplicar la función Λ a un tipo para obtener una función λ que aplique a un valor. Así por ejemplo:Haskell estándar (es decir, Haskell 98 y 2010) simplifica esto para usted al no tener ninguno de estos cuantificadores de tipo, lambdas mayúsculas y aplicaciones de tipo, pero detrás de escena GHC los coloca cuando analiza el programa para su compilación. (Todo esto es material en tiempo de compilación, creo, sin sobrecarga de tiempo de ejecución).
Pero el manejo automático de Haskell de esto significa que asume que "∀" nunca aparece en la rama izquierda de un tipo de función ("→").
Rank2TypesyRankNTypesdesactive esas restricciones y le permita anular las reglas predeterminadas de Haskell sobre dónde insertarforall.Por qué querrías hacer esto? Porque el Sistema F completo y sin restricciones es muy poderoso y puede hacer muchas cosas interesantes. Por ejemplo, la ocultación de tipos y la modularidad se pueden implementar utilizando tipos de rango superior. Tomemos, por ejemplo, una función antigua simple del siguiente tipo de rango 1 (para establecer la escena):
Para usar
f, la persona que llama primero debe elegir qué tipos usarryaluego proporcionar un argumento del tipo resultante. Entonces podrías elegirr = Intya = String:Pero ahora compare eso con el siguiente tipo de rango superior:
¿Cómo funciona una función de este tipo? Bueno, para usarlo, primero debes especificar qué tipo usar
r. Digamos que elegimosInt:Pero ahora
∀aestá dentro de la flecha de función, por lo que no puede elegir qué tipo usara; debe aplicarf' Inta una función of del tipo apropiado. Esto significa que la implementación def'elige qué tipo usara, no quién llamaf'. Sin tipos de rango superior, por el contrario, la persona que llama siempre elige los tipos.¿Para qué sirve esto? Bueno, para muchas cosas en realidad, pero una idea es que puede usar esto para modelar cosas como la programación orientada a objetos, donde los "objetos" agrupan algunos datos ocultos junto con algunos métodos que funcionan con los datos ocultos. Entonces, por ejemplo, un objeto con dos métodos, uno que devuelve una
Inty otro que devuelve unaString, podría implementarse con este tipo:¿Como funciona esto? El objeto se implementa como una función que tiene algunos datos internos de tipo oculto
a. Para utilizar realmente el objeto, sus clientes pasan una función de "devolución de llamada" que el objeto llamará con los dos métodos. Por ejemplo:Aquí estamos, básicamente, invocando el segundo método del objeto, aquel cuyo tipo es
a → Stringpara un desconocidoa. Bueno, desconocido paramyObjectlos clientes; pero estos clientes saben, por la firma, que podrán aplicarle cualquiera de las dos funciones y obtener unaInto unaString.Para un ejemplo real de Haskell, a continuación se muestra el código que escribí cuando aprendí por mi cuenta
RankNTypes. Esto implementa un tipo llamadoShowBoxque agrupa un valor de algún tipo oculto junto con suShowinstancia de clase. Tenga en cuenta que en el ejemplo de la parte inferior, hago una lista deShowBoxcuyo primer elemento se hizo a partir de un número y el segundo de una cadena. Dado que los tipos están ocultos al usar los tipos de rango superior, esto no viola la verificación de tipos.PD: para cualquiera que lea esto y se pregunte cómo es que se
ExistentialTypesusa GHCforall, creo que la razón es porque está usando este tipo de técnica detrás de escena.fuente
existspalabra clave, podría definir un tipo existencial como (por ejemplo)data Any = Any (exists a. a), dondeAny :: (exists a. a) -> Any. Usando ∀xP (x) → Q ≡ (∃xP (x)) → Q, podemos concluir queAnytambién podría tener un tipoforall a. a -> Anyy de ahíforallproviene la palabra clave. Creo que los tipos existenciales implementados por GHC son solo tipos de datos ordinarios que también contienen todos los diccionarios de clases de tipos requeridos (no pude encontrar una referencia para respaldar esto, lo siento).data ApplyBox r = forall a. ApplyBox (a -> r) a; cuando el patrón coincide conApplyBox f x, obtienef :: h -> ryx :: hpara un tipo restringido "oculto"h. Si entiendo bien, el caso del diccionario de la clase de tipos se traduce a algo como esto:data ShowBox = forall a. Show a => ShowBox ase traduce a algo comodata ShowBox' = forall a. ShowBox' (ShowDict' a) a;instance Show ShowBox' where show (ShowBox' dict val) = show' dict val;show' :: ShowDict a -> a -> String.La respuesta de Luis Casillas brinda mucha información excelente sobre lo que significan los tipos de rango 2, pero solo ampliaré un punto que no cubrió. Requerir que un argumento sea polimórfico no solo permite que se use con múltiples tipos; también restringe lo que esa función puede hacer con su (s) argumento (s) y cómo puede producir su resultado. Es decir, le da a la persona que llama menos flexibilidad. ¿Por qué querrías hacer eso? Comenzaré con un ejemplo simple:
Supongamos que tenemos un tipo de datos
y queremos escribir una función
que toma una función que se supone debe elegir uno de los elementos de la lista que se le da y devolver una
IOacción que lanza misiles a ese objetivo. Podríamos darfun tipo simple:El problema es que podríamos ejecutar accidentalmente
¡Y entonces estaríamos en un gran problema! Dar
fun tipo polimórfico de rango 1no ayuda en absoluto, porque elegimos el tipo
acuando llamamosf, y simplemente lo especializamosCountryy usamos nuestro malware\_ -> BestAllynuevamente. La solución es utilizar un tipo de rango 2:Ahora se requiere que la función que pasamos sea polimórfica, ¡así
\_ -> BestAllyque no escribiremos check! De hecho, ninguna función que devuelva un elemento que no esté en la lista que se le da hará una verificación de tipo (aunque algunas funciones que entran en bucles infinitos o producen errores y por lo tanto nunca regresan lo harán).Lo anterior es artificial, por supuesto, pero una variación de esta técnica es clave para hacer que la
STmónada sea segura.fuente
Los tipos de rango superior no son tan exóticos como han demostrado las otras respuestas. Lo crea o no, muchos lenguajes orientados a objetos (¡incluidos Java y C #!) Los incluyen. (Por supuesto, nadie en esas comunidades los conoce por el nombre que suena aterrador "tipos de rango superior").
El ejemplo que voy a dar es una implementación de libro de texto del patrón Visitor, que uso todo el tiempo en mi trabajo diario. Esta respuesta no pretende ser una introducción al patrón de visitantes; que el conocimiento es fácilmente disponible en otros lugares .
En esta fatua aplicación de recursos humanos imaginaria, deseamos operar con empleados que pueden ser empleados permanentes a tiempo completo o contratistas temporales. Mi variante preferida del patrón de visitante (y de hecho la que es relevante para
RankNTypes) parametriza el tipo de retorno del visitante.El punto es que varios visitantes con diferentes tipos de retorno pueden operar todos con los mismos datos. Este medio no
IEmployeedebe expresar ninguna opinión sobre lo queTdebería ser.Deseo llamar su atención sobre los tipos. Observe que
IEmployeeVisitorcuantifica universalmente su tipo de retorno, mientras que loIEmployeecuantifica dentro de suAcceptmétodo, es decir, en un rango superior. Traduciendo torpemente de C # a Haskell:Así que ahí lo tienes. Los tipos de rango superior aparecen en C # cuando escribe tipos que contienen métodos genéricos.
fuente
Las diapositivas del curso Haskell de Bryan O'Sullivan en Stanford me ayudaron a entender
Rank2Types.fuente
Para aquellos familiarizados con los lenguajes orientados a objetos, una función de rango superior es simplemente una función genérica que espera como argumento otra función genérica.
Por ejemplo, en TypeScript podría escribir:
¿Ves cómo el tipo de función genérica
Identifyexige una función genérica del tipoIdentifier? Esto haceIdentifyuna función de rango superior.fuente
Accepttiene un tipo polimórfico de rango 1, pero es un método deIEmployee, que es en sí mismo rango 2. Si alguien me da unIEmployee, puedo abrirlo y usar suAcceptmétodo en cualquier tipo.Visiteeclase que presenta. Una funciónf :: Visitee e => T ees (una vez desazucarado el material de la clase) esencialmentef :: (forall r. e -> Visitor e r -> r) -> T e. Haskell 2010 te permite salirte con la tuya con un polimorfismo de rango 2 limitado usando clases como esa.forallen mi ejemplo. No tengo una referencia a mano, pero es posible que encuentre algo en "Elimine sus clases de tipos" . El polimorfismo de rango superior puede introducir problemas de verificación de tipos, pero el tipo limitado implícito en el sistema de clases está bien.