Qual è lo scopo di Rank2Types?


110

Non sono molto esperto in Haskell, quindi questa potrebbe essere una domanda molto facile.

Quale limitazione linguistica risolve Rank2Types ? Le funzioni in Haskell non supportano già argomenti polimorfici?


È fondamentalmente un aggiornamento dal sistema di tipo HM al lambda calcolo polimorfico aka. λ2 / Sistema F. Tieni presente che inferenza di tipo indecidibile in λ2.
Poscat

Risposte:


116

Le funzioni in Haskell non supportano già argomenti polimorfici?

Lo fanno, ma solo di rango 1. Ciò significa che mentre puoi scrivere una funzione che accetta diversi tipi di argomenti senza questa estensione, non puoi scrivere una funzione che utilizza il suo argomento come tipi diversi nella stessa chiamata.

Ad esempio, la seguente funzione non può essere digitata senza questa estensione perché gviene utilizzata con diversi tipi di argomenti nella definizione di f:

f g = g 1 + g "lala"

Nota che è perfettamente possibile passare una funzione polimorfica come argomento a un'altra funzione. Quindi qualcosa di simile map id ["a","b","c"]è perfettamente legale. Ma la funzione può usarlo solo come monomorfico. Nell'esempio maputilizza idcome se avesse tipo String -> String. E ovviamente puoi anche passare una semplice funzione monomorfica del tipo dato invece di id. Senza rank2types non c'è modo per una funzione di richiedere che il suo argomento debba essere una funzione polimorfica e quindi non c'è nemmeno modo di usarlo come funzione polimorfica.


5
Per aggiungere alcune parole che collegano la mia risposta a questa: considera la funzione Haskell f' g x y = g x + g y. Il suo tipo di rango 1 dedotto è forall a r. Num r => (a -> r) -> a -> a -> r. Poiché forall aè al di fuori delle frecce della funzione, il chiamante deve prima scegliere un tipo per a; se scelgono Int, otteniamo f' :: forall r. Num r => (Int -> r) -> Int -> Int -> r, e ora abbiamo corretto l' gargomento in modo che possa richiedere Intma non String. Se abilitiamo RankNTypespossiamo annotare f'con type forall b c r. Num r => (forall a. a -> r) -> b -> c -> r. Non posso usarlo, però, cosa sarebbe g?
Luis Casillas

166

È difficile capire il polimorfismo di rango superiore a meno che non si studia il sistema F. direttamente il , perché Haskell è progettato per nasconderti i dettagli nell'interesse della semplicità.

Ma fondamentalmente, l'idea approssimativa è che i tipi polimorfici non hanno realmente la a -> bforma che hanno in Haskell; in realtà si presentano così, sempre con quantificatori espliciti:

id :: a.a  a
id = Λtx:t.x

Se non conosci il simbolo "∀", viene letto come "per tutti"; ∀x.dog(x)significa "poiché x, x è un cane". "Λ" è il lambda maiuscolo, utilizzato per astrarre i parametri di tipo; quello che dice la seconda riga è che id è una funzione che accetta un tipo te quindi restituisce una funzione parametrizzata da quel tipo.

Vedi, nel sistema F, non puoi semplicemente applicare una funzione del genere ida un valore subito; per prima cosa devi applicare la funzione Λ a un tipo per ottenere una funzione λ che applichi a un valore. Quindi per esempio:

tx:t.x) Int 5 = x:Int.x) 5
                  = 5

Standard Haskell (cioè, Haskell 98 e 2010) semplifica questo per te non avendo nessuno di questi quantificatori di tipo, lambda maiuscole e applicazioni di tipo, ma dietro le quinte GHC li inserisce quando analizza il programma per la compilazione. (Questa è tutta roba in fase di compilazione, credo, senza overhead di runtime.)

Ma la gestione automatica di Haskell di questo significa che assume che "∀" non appare mai sul ramo di sinistra di un tipo di funzione ("→"). Rank2Typese RankNTypesdisattivare queste restrizioni e consentire di ignorare le regole predefinite di Haskell per dove inserireforall .

Perché dovresti farlo? Perché il Sistema F completo e illimitato è estremamente potente e può fare molte cose interessanti. Ad esempio, l'occultamento del tipo e la modularità possono essere implementati utilizzando tipi di rango superiore. Prendiamo ad esempio una semplice vecchia funzione del seguente tipo di rango 1 (per impostare la scena):

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

Per utilizzarlo f, il chiamante deve prima scegliere quali tipi utilizzare re a, quindi fornire un argomento del tipo risultante. Quindi potresti scegliere r = Inte a = String:

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

Ma ora confrontalo con il seguente tipo di rango superiore:

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

Come funziona una funzione di questo tipo? Bene, per usarlo, prima devi specificare per quale tipo usare r. Diciamo di scegliere Int:

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

Ma ora si ∀atrova all'interno della freccia della funzione, quindi non puoi scegliere quale tipo utilizzare a; è necessario applicare f' Inta una funzione Λ del tipo appropriato. Ciò significa che l'implementazione di f'deve scegliere il tipo da utilizzare a, non il chiamantef' . Senza tipi di rango superiore, al contrario, il chiamante sceglie sempre i tipi.

A cosa serve? Bene, per molte cose in realtà, ma un'idea è che puoi usarlo per modellare cose come la programmazione orientata agli oggetti, dove gli "oggetti" raggruppano alcuni dati nascosti insieme ad alcuni metodi che lavorano sui dati nascosti. Quindi, ad esempio, un oggetto con due metodi, uno che restituisce an Inte un altro che restituisce a String, potrebbe essere implementato con questo tipo:

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

Come funziona? L'oggetto è implementato come una funzione che ha alcuni dati interni di tipo nascosto a. Per utilizzare effettivamente l'oggetto, i suoi client passano una funzione di "callback" che l'oggetto chiamerà con i due metodi. Per esempio:

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

Qui stiamo, fondamentalmente, invocando il secondo metodo dell'oggetto, quello il cui tipo è a → Stringper uno sconosciuto a. Ebbene, sconosciuto ai myObjectclienti di; ma questi clienti sanno, dalla firma, che saranno in grado di applicare una delle due funzioni ad essa e ottenere una Into unaString .

Per un esempio Haskell reale, di seguito è riportato il codice che ho scritto quando ho imparato da solo RankNTypes. Questo implementa un tipo chiamato ShowBoxche raggruppa un valore di un tipo nascosto insieme alla sua Showistanza di classe. Nota che nell'esempio in basso, creo un elenco di ShowBoxcui il primo elemento è stato composto da un numero e il secondo da una stringa. Poiché i tipi vengono nascosti utilizzando i tipi di rango più elevato, ciò non viola il controllo del tipo.

{-# 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: per chiunque stia leggendo questo articolo e si sia chiesto come ExistentialTypesmai usa GHC forall, credo che il motivo sia perché sta usando questo tipo di tecnica dietro le quinte.


2
Grazie per una risposta molto elaborata! (che, per inciso, alla fine mi ha anche motivato a imparare la teoria dei tipi corretta e il sistema F.)
Aleksandar Dimitrov

5
Se avessi una existsparola chiave, potresti definire un tipo esistenziale come (ad esempio) data Any = Any (exists a. a), dove Any :: (exists a. a) -> Any. Usando ∀xP (x) → Q ≡ (∃xP (x)) → Q, possiamo concludere che Anypotrebbe anche avere un tipo forall a. a -> Anyed è da lì che forallproviene la parola chiave. Credo che i tipi esistenziali implementati da GHC siano solo normali tipi di dati che contengono anche tutti i dizionari della classe di caratteri richiesti (non sono riuscito a trovare un riferimento per supportarlo, mi dispiace).
Vitus

2
@Vitus: gli esistenziali GHC non sono legati ai dizionari della classe di caratteri. Puoi avere data ApplyBox r = forall a. ApplyBox (a -> r) a; quando si esegue la corrispondenza del pattern ApplyBox f x, si ottiene f :: h -> re x :: hper un tipo "nascosto" limitato h. Se ho capito bene, il caso del dizionario typeclass è tradotto in qualcosa di simile: data ShowBox = forall a. Show a => ShowBox aè tradotto in qualcosa di simile data ShowBox' = forall a. ShowBox' (ShowDict' a) a; instance Show ShowBox' where show (ShowBox' dict val) = show' dict val; show' :: ShowDict a -> a -> String.
Luis Casillas

Questa è un'ottima risposta su cui dovrò dedicare del tempo. Penso di essere troppo abituato alle astrazioni fornite dai generici di C #, quindi ne davo molto per scontato invece di comprendere effettivamente la teoria.
Andrey Shchekin

@sacundim: Beh, "tutti i dizionari della classe di caratteri richiesti" può anche significare nessun dizionario se non ne hai bisogno. :) Il punto era che GHC molto probabilmente non codifica tipi esistenziali tramite tipi di grado più alto (cioè la trasformazione che suggerisci - ∃xP (x) ~ ∀r. (∀xP (x) → r) → r).
Vitus

47

La risposta di Luis Casillas fornisce molte ottime informazioni su cosa significano i tipi di rango 2, ma mi limiterò a espandere su un punto che non ha trattato. Richiedere che un argomento sia polimorfico non consente solo di utilizzarlo con più tipi; limita anche ciò che quella funzione può fare con i suoi argomenti e come può produrre il suo risultato. Cioè, offre al chiamante meno flessibilità. Perché dovresti farlo? Inizierò con un semplice esempio:

Supponiamo di avere un tipo di dati

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

e vogliamo scrivere una funzione

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

che assume una funzione che dovrebbe scegliere uno degli elementi della lista che gli viene data e restituire IOun'azione lanciando missili su quel bersaglio. Potremmo dare fun tipo semplice:

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

Il problema è che potremmo correre accidentalmente

f (\_ -> BestAlly)

e poi saremmo in grossi guai! Dare fun tipo polimorfico di grado 1

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

non aiuta affatto, perché scegliamo il tipo aquando chiamiamo f, e lo specializziamo Countrye usiamo di \_ -> BestAllynuovo il nostro malevolo . La soluzione è utilizzare un tipo di rango 2:

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

Ora la funzione che passiamo deve essere polimorfa, quindi \_ -> BestAllynon digitare check! In effetti, nessuna funzione che restituisce un elemento non presente nella lista che le viene data effettuerà il controllo del tipo (sebbene alcune funzioni che entrano in cicli infiniti o producono errori e quindi non tornano mai lo faranno).

Quanto sopra è artificioso, ovviamente, ma una variazione di questa tecnica è la chiave per rendere STsicura la monade.


18

I tipi di rango superiore non sono così esotici come hanno fatto le altre risposte. Che tu ci creda o no, molti linguaggi orientati agli oggetti (inclusi Java e C #!) Li presentano. (Naturalmente, nessuno in quelle comunità li conosce con il nome spaventoso "tipi di rango superiore".)

L'esempio che fornirò è un'implementazione da manuale del modello Visitor, che uso sempre nel mio lavoro quotidiano. Questa risposta non è intesa come un'introduzione al modello di visitatore; quella conoscenza è prontamente disponibile altrove .

In questa fatua e immaginaria applicazione delle risorse umane, desideriamo operare su dipendenti che possono essere dipendenti a tempo pieno o appaltatori temporanei. La mia variante preferita del pattern Visitor (e in effetti quella per cui è rilevante RankNTypes) parametrizza il tipo di ritorno del visitatore.

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

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

Il punto è che un numero di visitatori con diversi tipi di ritorno possono tutti operare sugli stessi dati. Ciò significa che non IEmployeedeve esprimere alcuna opinione su ciò che Tdovrebbe essere.

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);
    }
}

Desidero attirare la vostra attenzione sui tipi. Si noti che IEmployeeVisitorquantifica universalmente il suo tipo di ritorno, mentre lo IEmployeequantifica all'interno del suo Acceptmetodo, vale a dire a un livello più alto. Tradurre clunkily da C # a Haskell:

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

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

Così il gioco è fatto. I tipi di rango più elevato vengono visualizzati in C # quando si scrivono tipi contenenti metodi generici.


1
Vorrei sapere se qualcun altro ha scritto sul supporto di C # / Java / Blub per i tipi di rango superiore. Se tu, caro lettore, sei a conoscenza di tali risorse, per favore mandale a modo mio!
Benjamin Hodgson


-2

Per chi ha familiarità con i linguaggi orientati agli oggetti, una funzione di rango più elevato è semplicemente una funzione generica che si aspetta come argomento un'altra funzione generica.

Ad esempio in TypeScript potresti scrivere:

type WithId<T> = T & { id: number }
type Identifier = <T>(obj: T) => WithId<T>
type Identify = <TObj>(obj: TObj, f: Identifier) => WithId<TObj>

Vedi come il tipo di funzione generico Identifyrichiede una funzione generica del tipo Identifier? Ciò crea Identifyuna funzione di rango superiore.


Cosa aggiunge questo alla risposta di sepp2k?
dfeuer

O di Benjamin Hodgson, se è per questo?
dfeuer

1
Penso che tu abbia perso il punto di Hodgson. Acceptha un tipo polimorfico di rango 1, ma è un metodo di IEmployee, che è esso stesso di rango 2. Se qualcuno me ne dà un IEmployee, posso aprirlo e usare il suo Acceptmetodo in qualsiasi tipo.
dfeuer

1
Il tuo esempio è anche il grado 2, a titolo della Visiteeclasse che introduci. Una funzione f :: Visitee e => T eè (una volta che la roba della classe è stata desugared) essenzialmente f :: (forall r. e -> Visitor e r -> r) -> T e. Haskell 2010 ti consente di cavartela con il polimorfismo di rango 2 limitato usando classi come questa.
dfeuer

1
Non puoi tirare fuori il forallnel mio esempio. Non ho un riferimento fuori mano, ma potresti trovare qualcosa in "Scrap Your Type Classes" . Il polimorfismo di rango superiore può effettivamente introdurre problemi di controllo del tipo, ma l'ordinamento limitato implicito nel sistema di classi va bene.
dfeuer
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.