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
g
se 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 semap
usaid
como 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 a
está 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 elg
argumento para que pueda tomarInt
pero noString
. Si lo habilitamosRankNTypes
podemos 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 -> b
forma 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 tipot
y 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
id
a 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 ("→").
Rank2Types
yRankNTypes
desactive 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 usarr
ya
luego proporcionar un argumento del tipo resultante. Entonces podrías elegirr = Int
ya = 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
∀a
está dentro de la flecha de función, por lo que no puede elegir qué tipo usara
; debe aplicarf' Int
a 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
Int
y 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 → String
para un desconocidoa
. Bueno, desconocido paramyObject
los clientes; pero estos clientes saben, por la firma, que podrán aplicarle cualquiera de las dos funciones y obtener unaInt
o 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 llamadoShowBox
que agrupa un valor de algún tipo oculto junto con suShow
instancia de clase. Tenga en cuenta que en el ejemplo de la parte inferior, hago una lista deShowBox
cuyo 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
ExistentialTypes
usa GHCforall
, creo que la razón es porque está usando este tipo de técnica detrás de escena.fuente
exists
palabra 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 queAny
también podría tener un tipoforall a. a -> Any
y de ahíforall
proviene 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 -> r
yx :: h
para 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 a
se 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
IO
acción que lanza misiles a ese objetivo. Podríamos darf
un tipo simple:El problema es que podríamos ejecutar accidentalmente
¡Y entonces estaríamos en un gran problema! Dar
f
un tipo polimórfico de rango 1no ayuda en absoluto, porque elegimos el tipo
a
cuando llamamosf
, y simplemente lo especializamosCountry
y usamos nuestro malware\_ -> BestAlly
nuevamente. La solución es utilizar un tipo de rango 2:Ahora se requiere que la función que pasamos sea polimórfica, ¡así
\_ -> BestAlly
que 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
ST
mó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
IEmployee
debe expresar ninguna opinión sobre lo queT
debería ser.Deseo llamar su atención sobre los tipos. Observe que
IEmployeeVisitor
cuantifica universalmente su tipo de retorno, mientras que loIEmployee
cuantifica dentro de suAccept
mé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
Identify
exige una función genérica del tipoIdentifier
? Esto haceIdentify
una función de rango superior.fuente
Accept
tiene 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 suAccept
método en cualquier tipo.Visitee
clase que presenta. Una funciónf :: Visitee e => T e
es (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.forall
en 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.