web-dev-qa-db-fra.com

Quel est le but de Rank2Types?

Je ne suis pas vraiment compétent en Haskell, donc cela pourrait être une question très facile.

Quelle limitation de langue Rank2Types résout-il? Les fonctions de Haskell ne prennent-elles pas déjà en charge les arguments polymorphes?

104
Andrey Shchekin

Les fonctions de Haskell ne prennent-elles pas déjà en charge les arguments polymorphes?

Ils le font, mais uniquement de rang 1. Cela signifie que même 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 saisie sans cette extension car g est utilisé avec différents types d'arguments dans la définition de f:

f g = g 1 + g "lala"

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 utilise id comme s'il avait le type String -> String. Et bien sûr, vous pouvez également passer une simple fonction monomorphe du type donné au lieu de id. Sans types rank2, 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.

111
sepp2k

Il est difficile de comprendre le polymorphisme de rang supérieur à moins d'étudier directement Système F , car Haskell est conçu pour vous en cacher les détails dans un souci de simplicité.

Mais en gros, l'idée approximative est que les types polymorphes n'ont pas vraiment la forme a -> b Qu'ils ont dans Haskell; en réalité, ils ressemblent à ça, toujours avec des quantificateurs explicites:

id :: ∀a.a → a
id = Λt.λx:t.x

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 lambda majuscule, utilisé pour extraire les paramètres de type; ce que la deuxième ligne dit, c'est que id est une fonction qui prend un type t, puis retourne une fonction qui est paramétrée par ce type.

Vous voyez, dans System F, vous ne pouvez pas simplement appliquer une fonction comme ça id à une valeur tout de suite; vous devez d'abord appliquer la fonction to à un type afin d'obtenir une fonction λ que vous appliquez à une valeur. Ainsi, par exemple:

(Λt.λx:t.x) Int 5 = (λx:Int.x) 5
                  = 5

Standard Haskell (c'est-à-dire Haskell 98 et 2010) simplifie cela pour vous en ne disposant d'aucun de ces quantificateurs de type, lambda capital et applications de type, mais en arrière-plan, GHC les met lorsqu'il analyse le programme pour la compilation. (Je crois que ce sont des choses au moment de la compilation, sans surcharge d'exécution.)

Mais la gestion automatique de cela par Haskell signifie qu'il suppose que "∀" n'apparaît jamais sur la branche gauche d'un type de fonction ("→"). Rank2Types Et RankNTypes désactivent ces restrictions et vous permettent de remplacer les règles par défaut de Haskell pour savoir où insérer forall.

Pourquoi voudriez-vous faire ça? Parce que le système F complet et sans restriction est extrêmement puissant et qu'il 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):

f :: ∀r.∀a.((a → r) → a → r) → r

Pour utiliser f, l'appelant doit d'abord choisir les types à utiliser pour r et a, puis fournir un argument du type résultant. Vous pouvez donc choisir r = Int Et a = String:

f Int String :: ((String → Int) → String → Int) → Int

Mais maintenant, comparez cela au type de rang supérieur suivant:

f' :: ∀r.(∀a.(a → r) → a → r) → r

Comment fonctionne une fonction de ce type? Eh bien, pour l'utiliser, vous devez d'abord spécifier le type à utiliser pour r. Disons que nous choisissons Int:

f' Int :: (∀a.(a → Int) → a → Int) → Int

Mais maintenant, le ∀a Est à l'intérieur la flèche de fonction, vous ne pouvez donc pas choisir le type à utiliser pour a; vous devez appliquer f' Int à une fonction of du type approprié. Cela signifie que l'implémentation de f' Arrive à choisir le type à utiliser pour a, pas l'appelant de f'. 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, l'une renvoyant un Int et l'autre renvoyant un String, pourrait être implémenté avec ce type:

myObject :: ∀r.(∀a.(a → Int, a -> String) → a → r) → r

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 transmettent une fonction de "rappel" que l'objet appellera avec les deux méthodes. Par exemple:

myObject String (Λa. λ(length, name):(a → Int, a → String). λobjData:a. name objData)

Ici, nous invoquons fondamentalement la deuxième méthode de l'objet, celle dont le type est a → String Pour un a inconnu. Eh bien, inconnu des clients de myObject; mais ces clients savent, grâce à la signature, qu'ils pourront lui appliquer l'une des deux fonctions et obtenir soit un Int soit un String.

Pour un exemple réel de Haskell, voici 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 son instance de classe Show. Notez que dans l'exemple en bas, je fais une liste de ShowBox dont le premier élément a été fait à partir d'un nombre, et le second à partir 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.

{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ImpredicativeTypes #-}

type ShowBox = forall b. (forall a. Show a => a -> b) -> b

mkShowBox :: Show a => a -> ShowBox
mkShowBox x = \k -> k x

-- | This is the key function for using a 'ShowBox'.  You pass in
-- a function @k@ that will be applied to the contents of the 
-- ShowBox.  But you don't pick the type of @k@'s argument--the 
-- ShowBox does.  However, it's restricted to picking a type that
-- implements @Show@, so you know that whatever type it picks, you
-- can use the 'show' function.
runShowBox :: forall b. (forall a. Show a => a -> b) -> ShowBox -> b
-- Expanded type:
--
--     runShowBox 
--         :: forall b. (forall a. Show a => a -> b) 
--                   -> (forall b. (forall a. Show a => a -> b) -> b)
--                   -> b
--
runShowBox k box = box k


example :: [ShowBox] 
-- example :: [ShowBox] expands to this:
--
--     example :: [forall b. (forall a. Show a => a -> b) -> b]
--
-- Without the annotation the compiler infers the following, which
-- breaks in the definition of 'result' below:
--
--     example :: forall b. [(forall a. Show a => a -> b) -> b]
--
example = [mkShowBox 5, mkShowBox "foo"]

result :: [String]
result = map (runShowBox show) example

PS: pour tous ceux qui lisent ceci et qui se demandent comment se fait que ExistentialTypes dans GHC utilise forall, je crois que la raison est parce qu'il utilise ce type de technique en coulisses.

155
Luis Casillas

réponse de Luis Casillas donne beaucoup d'informations sur la signification des types de rang 2, mais je vais juste développer un point qu'il n'a pas couvert. Exiger qu'un argument soit polymorphe ne permet pas seulement de l'utiliser 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 à l'appelant moins de flexibilité . Pourquoi voudriez-vous faire ça? Je vais commencer par un exemple simple:

Supposons que nous ayons un type de données

data Country = BigEnemy | MediumEnemy | PunyEnemy | TradePartner | Ally | BestAlly

et nous voulons écrire une fonction

f g = launchMissilesAt $ g [BigEnemy, MediumEnemy, PunyEnemy]

qui prend une fonction qui est censée choisir l'un des éléments de la liste qui lui est donnée et retourner une action de lancement de missiles IO sur cette cible. Nous pourrions donner à f un type simple:

f :: ([Country] -> Country) -> IO ()

Le problème est que nous pourrions accidentellement courir

f (\_ -> BestAlly)

et nous aurions alors de gros ennuis! Donner à f un type polymorphe de rang 1

f :: ([a] -> a) -> IO ()

n'aide pas du tout, car nous choisissons le type a lorsque nous appelons f, et nous le spécialisons simplement dans Country et utilisons notre \_ -> BestAlly malveillant encore. La solution consiste à utiliser un type de rang 2:

f :: (forall a . [a] -> a) -> IO ()

Maintenant, la fonction que nous transmettons doit être polymorphe, donc \_ -> BestAlly Ne tapera pas check! En fait, aucune fonction renvoyant un élément ne figurant pas dans la liste qui lui est donnée sera vérifiée (bien que certaines fonctions qui entrent dans des boucles infinies ou produisent des erreurs et donc jamais retour le fera).

Bien sûr, ce qui précède est artificiel, mais une variation de cette technique est essentielle pour sécuriser la monade ST.

40
dfeuer

Les types de rang supérieur ne sont pas aussi exotiques que les autres réponses l'ont démontré. Croyez-le ou non, de nombreux langages orientés objet (y compris Java et C #!) Les présentent. (Bien sûr, personne dans ces communautés ne les connaît par le nom effrayant "de rang supérieur) les types".)

L'exemple que je vais donner est une implémentation de manuel du modèle Visitor, que j'utilise tout le temps dans mon travail quotidien. Cette réponse n'est pas conçue comme une introduction au modèle de visiteur; cette connaissance est facilementdisponibleailleurs .

Dans cette application RH imaginaire, nous souhaitons opérer sur des salariés qui peuvent être des permanents à plein temps ou des intérimaires. Ma variante préférée du modèle Visitor (et en effet celle qui concerne RankNTypes) paramètre le type de retour du visiteur.

interface IEmployeeVisitor<T>
{
    T Visit(PermanentEmployee e);
    T Visit(Contractor c);
}

class XmlVisitor : IEmployeeVisitor<string> { /* ... */ }
class PaymentCalculator : IEmployeeVisitor<int> { /* ... */ }

Le fait est qu'un certain nombre de visiteurs avec différents types de retours peuvent tous opérer sur les mêmes données. Cela signifie que IEmployee ne doit exprimer aucune opinion sur ce que T doit être.

interface IEmployee
{
    T Accept<T>(IEmployeeVisitor<T> v);
}
class PermanentEmployee : IEmployee
{
    // ...
    public T Accept<T>(IEmployeeVisitor<T> v)
    {
        return v.Visit(this);
    }
}
class Contractor : IEmployee
{
    // ...
    public T Accept<T>(IEmployeeVisitor<T> v)
    {
        return v.Visit(this);
    }
}

Je souhaite attirer votre attention sur les types. Remarquez que IEmployeeVisitor quantifie universellement son type de retour, tandis que IEmployee le quantifie à l'intérieur de sa méthode Accept - c'est-à-dire à un rang supérieur. Traduction clunkily de C # à Haskell:

data IEmployeeVisitor r = IEmployeeVisitor {
    visitPermanent :: PermanentEmployee -> r,
    visitContractor :: Contractor -> r
}

newtype IEmployee = IEmployee {
    accept :: forall r. IEmployeeVisitor r -> r
}

Alors voilà. Les types de rang supérieur apparaissent en C # lorsque vous écrivez des types contenant des méthodes génériques.

14
Benjamin Hodgson
5
moiseev