Je ne maîtrise pas vraiment Haskell, c'est peut-être une question très simple.
Quelles sont les limitations linguistiques résolues par Rank2Types ? Les fonctions dans Haskell ne prennent-elles pas déjà en charge les arguments polymorphes?
haskell
types
polymorphism
higher-rank-types
Andrey Shchekin
la source
la source
Réponses:
Ils le font, mais seulement de rang 1. Cela signifie que si vous pouvez écrire une fonction qui prend différents types d'arguments sans cette extension, vous ne pouvez pas écrire une fonction qui utilise son argument comme différents types dans la même invocation.
Par exemple, la fonction suivante ne peut pas être tapée sans cette extension car elle
g
est utilisée avec différents types d'arguments dans la définition def
:Notez qu'il est parfaitement possible de passer une fonction polymorphe comme argument à une autre fonction. Donc, quelque chose comme
map id ["a","b","c"]
est parfaitement légal. Mais la fonction ne peut l'utiliser que comme monomorphe. Dans l'exemple,map
utiliseid
comme s'il avait du typeString -> String
. Et bien sûr, vous pouvez également passer une simple fonction monomorphe du type donné à la place deid
. Sans rank2types, il n'y a aucun moyen pour une fonction d'exiger que son argument soit une fonction polymorphe et donc également aucun moyen de l'utiliser comme fonction polymorphe.la source
f' g x y = g x + g y
. Son type de rang 1 inféré estforall a r. Num r => (a -> r) -> a -> a -> r
. Puisqu'il seforall a
trouve en dehors des flèches de fonction, l'appelant doit d'abord choisir un type poura
; s'ils choisissentInt
, nous obtenonsf' :: forall r. Num r => (Int -> r) -> Int -> Int -> r
, et maintenant nous avons corrigé l'g
argument afin qu'il puisse prendreInt
mais pasString
. Si nous activons,RankNTypes
nous pouvons annoterf'
avec le typeforall b c r. Num r => (forall a. a -> r) -> b -> c -> r
. Je ne peux pas l'utiliser, cependant - que serait-ceg
?Il est difficile de comprendre le polymorphisme de rang supérieur à moins d'étudier directement le système F , car Haskell est conçu pour vous en cacher les détails dans un souci de simplicité.
Mais fondamentalement, l'idée approximative est que les types polymorphes n'ont pas vraiment la
a -> b
forme qu'ils ont dans Haskell; en réalité, ils ressemblent à ceci, toujours avec des quantificateurs explicites:Si vous ne connaissez pas le symbole "∀", il est lu comme "pour tous";
∀x.dog(x)
signifie "pour tout x, x est un chien." "Λ" est un lambda majuscule, utilisé pour l'abstraction des paramètres de type; ce que dit la deuxième ligne, c'est que id est une fonction qui prend un typet
, puis retourne une fonction paramétrée par ce type.Vous voyez, dans le système F, vous ne pouvez pas simplement appliquer une fonction comme celle-là
id
à une valeur tout de suite; vous devez d'abord appliquer la fonction Λ à un type afin d'obtenir une fonction λ que vous appliquez à une valeur. Donc par exemple:Standard Haskell (c'est-à-dire, Haskell 98 et 2010) simplifie cela pour vous en n'ayant aucun de ces quantificateurs de type, lambdas majuscules et applications de type, mais dans les coulisses, GHC les place lorsqu'il analyse le programme pour la compilation. (Ce sont tous des trucs de compilation, je crois, sans surcharge d'exécution.)
Mais la gestion automatique de cela par Haskell signifie qu'elle suppose que "∀" n'apparaît jamais sur la branche gauche d'une fonction ("→") de type.
Rank2Types
etRankNTypes
désactivez ces restrictions et vous permettre de remplacer les règles par défaut de Haskell pour savoir où insérerforall
.Pourquoi voudriez-vous faire ça? Parce que le système F complet et sans restriction est extrêmement puissant et peut faire beaucoup de choses intéressantes. Par exemple, le masquage de type et la modularité peuvent être implémentés à l'aide de types de rang supérieur. Prenons par exemple une ancienne fonction simple du type de rang 1 suivant (pour définir la scène):
Pour utiliser
f
, l'appelant doit d' abord choisir les types à utiliser pourr
eta
, puis fournir un argument du type résultant. Vous pouvez donc choisirr = Int
eta = String
:Mais comparez maintenant cela au type de rang supérieur suivant:
Comment fonctionne une fonction de ce type? Eh bien, pour l'utiliser, spécifiez d'abord le type à utiliser
r
. Disons que nous choisissonsInt
:Mais maintenant, le
∀a
est à l' intérieur de la flèche de fonction, vous ne pouvez donc pas choisir le type à utilisera
; vous devez postulerf' Int
à une fonction Λ du type approprié. Cela signifie que l'implémentation def'
permet de choisir le type à utilisera
, pas l'appelant def'
. Sans types de rang supérieur, au contraire, l'appelant choisit toujours les types.À quoi cela sert-il? Eh bien, pour beaucoup de choses en fait, mais une idée est que vous pouvez l'utiliser pour modéliser des choses comme la programmation orientée objet, où les "objets" regroupent des données cachées avec des méthodes qui fonctionnent sur les données cachées. Ainsi, par exemple, un objet avec deux méthodes - une qui renvoie an
Int
et un autre qui renvoie aString
, pourrait être implémenté avec ce type:Comment cela marche-t-il? L'objet est implémenté comme une fonction qui a des données internes de type caché
a
. Pour utiliser réellement l'objet, ses clients passent une fonction de "callback" que l'objet appellera avec les deux méthodes. Par exemple:Ici, nous invoquons essentiellement la deuxième méthode de l'objet, celle dont le type est
a → String
pour une inconnuea
. Eh bien, inconnumyObject
des clients de; mais ces clients savent, d'après la signature, qu'ils pourront lui appliquer l'une ou l'autre des deux fonctions et obtenir soit unInt
soit unString
.Pour un exemple réel de Haskell, vous trouverez ci-dessous le code que j'ai écrit lorsque j'ai appris moi-même
RankNTypes
. Cela implémente un type appeléShowBox
qui regroupe une valeur d'un type caché avec sonShow
instance de classe. Notez que dans l'exemple en bas, je fais une listeShowBox
dont le premier élément a été fait à partir d'un nombre, et le second d'une chaîne. Étant donné que les types sont masqués à l'aide des types de rang supérieur, cela ne viole pas la vérification de type.PS: pour quiconque lit ceci et se demande comment se fait l'
ExistentialTypes
utilisation du GHCforall
, je pense que la raison en est qu'il utilise ce genre de technique en coulisses.la source
exists
mot-clé, vous pourriez définir un type existentiel comme (par exemple)data Any = Any (exists a. a)
, oùAny :: (exists a. a) -> Any
. En utilisant ∀xP (x) → Q ≡ (∃xP (x)) → Q, nous pouvons conclure que celaAny
pourrait aussi avoir un typeforall a. a -> Any
et c'est de là queforall
vient le mot - clé. Je crois que les types existentiels tels qu'implémentés par GHC ne sont que des types de données ordinaires qui portent également tous les dictionnaires de classes de types requis (je n'ai pas trouvé de référence pour le sauvegarder, désolé).data ApplyBox r = forall a. ApplyBox (a -> r) a
; lorsque vous correspondez à un motifApplyBox f x
, vous obtenezf :: h -> r
etx :: h
pour un type restreint "masqué"h
. Si je comprends bien, le cas du dictionnaire de classes de types est traduit en quelque chose comme ceci:data ShowBox = forall a. Show a => ShowBox a
est traduit en quelque chose commedata ShowBox' = forall a. ShowBox' (ShowDict' a) a
;instance Show ShowBox' where show (ShowBox' dict val) = show' dict val
;show' :: ShowDict a -> a -> String
.La réponse de Luis Casillas donne beaucoup d'informations sur ce que signifient les types de rang 2, mais je vais simplement développer un point qu'il n'a pas couvert. Exiger qu'un argument soit polymorphe ne lui permet pas seulement d'être utilisé avec plusieurs types; il restreint également ce que cette fonction peut faire avec ses arguments et comment elle peut produire son résultat. Autrement dit, cela donne moins de flexibilité à l'appelant . Pourquoi voudriez-vous faire ça? Je vais commencer par un exemple simple:
Supposons que nous ayons un type de données
et nous voulons écrire une fonction
qui prend une fonction qui est censée choisir l'un des éléments de la liste qui lui est donnée et renvoyer une
IO
action lançant des missiles sur cette cible. Nous pourrions donnerf
un type simple:Le problème est que nous pourrions courir accidentellement
et alors nous aurions de gros ennuis! Donnant
f
un type polymorphe de rang 1n'aide pas du tout, car nous choisissons le type
a
lorsque nous appelonsf
, et nous le spécialisons simplementCountry
et utilisons notre\_ -> BestAlly
nouveau . La solution est d'utiliser un type de rang 2:Maintenant, la fonction que nous transmettons doit être polymorphe, donc
\_ -> BestAlly
ne tapez pas check! En fait, aucune fonction retournant un élément ne figurant pas dans la liste qui lui est donnée ne sera typé (bien que certaines fonctions qui entrent dans des boucles infinies ou produisent des erreurs et donc ne reviennent jamais le feront).Ce qui précède est artificiel, bien sûr, mais une variante de cette technique est essentielle pour
ST
sécuriser la monade.la source
Les types de rang plus élevé ne sont pas aussi exotiques que les autres réponses l'ont laissé entendre. Croyez-le ou non, de nombreux langages orientés objet (y compris Java et C #!) Les intègrent. (Bien sûr, personne dans ces communautés ne les connaît sous le nom effrayant de "types de rang supérieur".)
L'exemple que je vais donner est une implémentation classique du modèle Visiteur, que j'utilise tout le temps dans mon travail quotidien. Cette réponse ne se veut pas une introduction au modèle de visiteur; cette connaissance est facilement disponible ailleurs .
Dans cette folle application RH imaginaire, nous souhaitons opérer sur des salariés qui peuvent être permanents à temps plein ou intérimaires. Ma variante préférée du modèle de visiteur (et en fait celle qui est pertinente pour
RankNTypes
) paramètre le type de retour du visiteur.Le fait est qu'un certain nombre de visiteurs avec des types de retour différents peuvent tous opérer sur les mêmes données. Ce moyen ne
IEmployee
doit exprimer aucune opinion sur ce quiT
devrait être.Je souhaite attirer votre attention sur les types. Observez que
IEmployeeVisitor
quantifie universellement son type de retour, alors que leIEmployee
quantifie à l'intérieur de saAccept
méthode - c'est-à-dire à un rang supérieur. Traduire maladroitement de C # à Haskell:Alors là vous l'avez. Les types de rang supérieur apparaissent en C # lorsque vous écrivez des types contenant des méthodes génériques.
la source
Les diapositives du cours Haskell de Bryan O'Sullivan à Stanford m'ont aidé à comprendre
Rank2Types
.la source
Pour ceux qui sont familiers avec les langages orientés objet, une fonction de rang supérieur est simplement une fonction générique qui attend comme argument une autre fonction générique.
Par exemple, dans TypeScript, vous pouvez écrire:
Voyez comment le type de fonction générique
Identify
exige une fonction générique du typeIdentifier
? Cela faitIdentify
une fonction de rang supérieur.la source
Accept
a un type polymorphe de rang 1, mais c'est une méthode deIEmployee
, qui est elle-même de rang 2. Si quelqu'un me donne unIEmployee
, je peux l'ouvrir et utiliser saAccept
méthode à n'importe quel type.Visitee
classe que vous introduisez. Une fonctionf :: Visitee e => T e
est (une fois que le truc de la classe est désucré) essentiellementf :: (forall r. e -> Visitor e r -> r) -> T e
. Haskell 2010 vous permet de vous en sortir avec un polymorphisme de rang 2 limité en utilisant des classes comme ça.forall
dans mon exemple. Je n'ai pas de référence, mais vous trouverez peut-être quelque chose dans "Scrap Your Type Classes" . Le polymorphisme de rang supérieur peut en effet introduire des problèmes de vérification de type, mais le tri limité implicite dans le système de classes est très bien.