Il polimorfismo parametrico di rango superiore è utile?


16

Sono abbastanza sicuro che tutti abbiano familiarità con i metodi generici del modulo:

T DoSomething<T>(T item)

Questa funzione è anche chiamata parametricamente polimorfica (PP), in particolare la PP di grado 1 .

Diciamo che questo metodo può essere rappresentato usando un oggetto funzione del modulo:

<T> : T -> T

Cioè, <T>significa che accetta un parametro di tipo e T -> Tindica che accetta un parametro di tipo Te restituisce un valore dello stesso tipo.

Quindi la seguente sarebbe una funzione PP di grado 2:

(<T> : T -> T) -> int 

La funzione non accetta parametri di tipo, ma accetta una funzione che accetta un parametro di tipo. Puoi continuare in modo iterativo, rendendo la nidificazione sempre più profonda, ottenendo PP di rango sempre più alto.

Questa funzionalità è davvero rara tra i linguaggi di programmazione. Anche Haskell non lo consente per impostazione predefinita.

È utile? Può descrivere comportamenti difficili da descrivere diversamente?

Inoltre, cosa significa che qualcosa è impredicativo ? (in tale contesto)


1
È interessante notare che TypeScript è un linguaggio mainstream con supporto PP completo di grado n. Ad esempio, quanto segue è un codice TypeScript valido:let sdff = (g : (f : <T> (e : T) => void) => void) => {}
GregRos

Risposte:


11

In generale, si utilizza il polimorfismo di rango superiore quando si desidera che il chiamante sia in grado di selezionare il valore di un parametro di tipo, anziché il chiamante . Per esempio:

f :: (forall a. Show a => a -> Int) -> (Int, Int)
f g = (g "one", g 2)

Qualsiasi funzione gche passo a questo fdeve essere in grado di darmi un Intvalore di un certo tipo, dove l' unica cosa a gconoscenza di quel tipo è che ha un'istanza di Show. Quindi questi sono kosher:

f (length . show)
f (const 42)

Ma questi non sono:

f length
f succ

Un'applicazione particolarmente utile è l'utilizzo dell'ambito dei tipi per imporre l'ambito dei valori . Supponiamo di avere un oggetto di tipo Action<T>, che rappresenta un'azione che possiamo eseguire per produrre un risultato di tipo T, come un futuro o un callback.

T runAction<T>(Action<T>)

runAction :: forall a. Action a -> a

Supponiamo ora di avere anche un oggetto in Actiongrado di allocare Resource<T>oggetti:

Action<Resource<T>> newResource<T>(T)

newResource :: forall a. a -> Action (Resource a)

Vogliamo imporre che tali risorse vengano utilizzate solo all'interno del luogo in Actioncui sono state create e non condivise tra azioni diverse o percorsi diversi della stessa azione, in modo che le azioni siano deterministiche e ripetibili.

A tale scopo, possiamo utilizzare tipi di livello superiore aggiungendo un parametro Sai tipi Resourcee Action, che è totalmente astratto: rappresenta lo "scopo" di Action. Ora le nostre firme sono:

T run<T>(<S> Action<S, T>)
Action<S, Resource<S, T>> newResource<T>(T)

runAction :: forall a. (forall s. Action s a) -> a
newResource :: forall s a. a -> Action s (Resource s a)

Ora, quando diamo runActionun Action<S, T>, ci viene assicurato che, poiché il parametro "scope" Sè completamente polimorfico, non può sfuggire al corpo di runAction- così nessun valore di un tipo che usa Scome Resource<S, int>similmente non può sfuggire!

(In Haskell, questa è conosciuta come la STmonade, dove runActionviene chiamata runST, Resourceviene chiamata STRefe newResourceviene chiamata newSTRef.)


La STmonade è un esempio molto interessante. Puoi fare qualche esempio in più di quando sarebbe utile il polimorfismo di rango superiore?
GregRos,

@GregRos: è utile anche con gli esistenziali. In Haxl , avevamo un like esistenziale data Fetch d = forall a. Fetch (d a) (MVar a), che è una coppia di una richiesta a un'origine dati de uno slot in cui archiviare il risultato. Il risultato e lo slot devono avere tipi corrispondenti, ma quel tipo è nascosto, quindi è possibile avere un elenco eterogeneo di richieste alla stessa origine dati. Ora è possibile utilizzare più alto rango il polimorfismo per scrivere una funzione che recupera tutte le richieste, data una funzione che recupera uno: fetch :: (forall a. d a -> IO a) -> [Fetch d] -> IO ().
Jon Purdy,

8

Il polimorfismo di rango superiore è estremamente utile. Nel Sistema F (il linguaggio di base dei linguaggi FP tipizzati che conosci), questo è essenziale per ammettere "codifiche della Chiesa tipizzate", che è in realtà il modo in cui il Sistema F esegue la programmazione. Senza questi, il sistema F è completamente inutile.

Nel Sistema F, definiamo i numeri come

Nat = forall c. (c -> c) -> c -> c

L'aggiunta ha il tipo

plus : Nat -> Nat -> Nat
plus l r = Λ t. λ (s : t -> t). λ (z : t). l s (r s z)

che è un tipo di rango superiore ( forall c.appare all'interno di quelle frecce).

Questo succede anche in altri posti. Ad esempio, se vuoi indicare che un calcolo è uno stile di passaggio di continuazione adeguato (google "codensity haskell"), lo avresti corretto come

type CPSed A = forall c. (A -> c) -> c

Anche parlare di un tipo disabitato nel Sistema F richiede un polimorfismo di rango superiore

type Void = forall a. a 

Il lungo e il corto di questo, scrivere una funzione in un sistema di tipo puro (Sistema F, CoC) richiede un polimorfismo di rango superiore se vogliamo trattare dati interessanti.

Nel sistema F in particolare, queste codifiche devono essere "impredicative". Ciò significa che un si forall a.quantifica assolutamente tutti i tipi . Ciò include in modo critico il tipo che stiamo definendo. In forall a. aquesto apotrebbe effettivamente rappresentare di forall a. anuovo! In linguaggi come ML non è questo il caso, si dice che siano "predicativi" poiché una variabile di tipo si quantifica solo sull'insieme di tipi senza quantificatori (chiamati monotipi). La nostra definizione di plusimpredicativity necessaria anche perché abbiamo istanziato l' cin l : Natessere Nat!

Infine, vorrei menzionare un'ultima ragione per cui si desidera sia l'impredicatività che il polimorfismo di rango superiore anche in una lingua con tipi arbitrariamente ricorsivi (a differenza del Sistema F). In Haskell, esiste una monade per effetti chiamata "monade thread di stato". L'idea è che la monade del thread di stato ti permetta di mutare le cose, ma richiede di sfuggire che il tuo risultato non dipende da nulla di mutabile. Ciò significa che i calcoli ST sono notevolmente puri. Per applicare questo requisito utilizziamo un polimorfismo di rango superiore

runST :: forall a. (forall s. ST s a) -> a

Qui assicurandoci che asia limitato al di fuori dell'ambito in cui vi presentiamo s, sappiamo che asta per un tipo ben formato su cui non si basa s. Usiamo sper paramerizzare tutte le cose mutabili in quel particolare filo di stato, quindi sappiamo che aè indipendente dalle cose mutabili e quindi che nulla sfugge all'ambito di tale STcalcolo! Un meraviglioso esempio dell'uso dei tipi per escludere programmi mal formati.

A proposito, se sei interessato a conoscere la teoria dei tipi, suggerirei di investire in un buon libro o due. È difficile imparare queste cose a pezzi. Suggerirei uno dei libri di Pierce o Harper sulla teoria del PL in generale (e alcuni elementi della teoria dei tipi). Il libro "Argomenti avanzati in tipi e linguaggi di programmazione" copre anche una buona parte della teoria dei tipi. Infine, "Programmare nella teoria dei tipi di Martin Lof" è un'ottima esposizione alla teoria del tipo intensionale delineata da Martin Lof.


Grazie per le vostre raccomandazioni. Li cercherò. L'argomento è davvero interessante e vorrei che alcuni più avanzati concetti di sistema di tipo fossero adottati da più linguaggi di programmazione. Ti danno molto più potere espressivo.
GregRos,
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.