Definisci un elenco usando solo il sistema di tipo Hindley-Milner


10

Sto lavorando su un piccolo compilatore di calcoli lambda che ha un sistema di inferenza di tipo Hindley-Milner funzionante e ora supporta anche un carattere ricorsivo (non nel codice collegato), che comprendo dovrebbe essere sufficiente per renderlo completo .

Il problema ora è che non ho idea di come renderli elenchi di supporto o se li supporta già e ho solo bisogno di trovare un modo per codificarli. Mi piacerebbe essere in grado di definirli senza dover aggiungere nuove regole al sistema di tipi.

Il modo più semplice in cui riesco a pensare a un elenco xè come qualcosa che è null(o l'elenco vuoto) o una coppia che contiene sia un xche un elenco x. Ma per fare questo devo essere in grado di definire coppie e o, che credo siano il prodotto e i tipi di somma.

Sembra che posso definire le coppie in questo modo:

pair = λabf.fab
first = λp.p(λab.a)
second = λp.p(λab.b)

Dato che pairavrebbe il tipo a -> (b -> ((a -> (b -> x)) -> x)), dopo aver passato, diciamo, an inte a string, produrrebbe qualcosa con type (int -> (string -> x)) -> x, che sarebbe la rappresentazione di una coppia di inte string. Ciò che mi preoccupa qui è che se ciò rappresenta una coppia, perché ciò non è logicamente equivalente, né implica la proposizione int and string? Tuttavia, equivale a (((int and string) -> x) -> x), come se potessi avere solo tipi di prodotto come parametri per le funzioni. Questa rispostasembra affrontare questo problema, ma non ho idea di cosa significhino i simboli che usa. Inoltre, se questo non codifica realmente un tipo di prodotto, c'è qualcosa che posso fare con i tipi di prodotto che non potrei fare con la mia definizione di coppie sopra (considerando che posso anche definire n-tuple allo stesso modo)? In caso contrario, ciò non contraddirebbe il fatto che non è possibile esprimere la congiunzione (AFAIK) usando solo l'implicazione?

Inoltre, che ne dici del tipo di somma? Posso in qualche modo codificarlo usando solo il tipo di funzione? In tal caso, basterebbe definire le liste? Altrimenti, c'è un altro modo per definire elenchi senza dover estendere il mio sistema di tipi? E se no, quali cambiamenti dovrei fare se voglio mantenerlo il più semplice possibile?

Tieni presente che sono un programmatore di computer ma non uno scienziato informatico né un matematico e piuttosto cattivo nel leggere la notazione matematica.

Modifica: non sono sicuro di quale sia il nome tecnico di ciò che ho implementato finora, ma tutto quello che ho è fondamentalmente il codice che ho collegato sopra, che è un algoritmo di generazione di vincoli che utilizza le regole per applicazioni, astrazioni e variabili prese dall'algoritmo Hinley-Milner e quindi un algoritmo di unificazione che ottiene il tipo principale. Ad esempio, l'espressione \a.aprodurrà il tipo a -> ae l'espressione \a.(a a)genererà un errore di verifica si verifica. Inoltre, non esiste esattamente una letregola ma una funzione che sembra avere lo stesso effetto che consente di definire funzioni globali ricorsive come questo pseudo-codice:

GetTypeOfGlobalFunction(term, globalScope, nameOfFunction)
{
    // Here 'globalScope' contains a list of name-value pair where every value is of class 'ClosedType', 
    // meaning their type will be cloned before unified in the unification algorithm so that they can be used polymorphically 
    tempType = new TypeVariable() // Assign a dummy type to `tempType`, say, type 'x'.
    // The next line creates an scope with everything in 'globalScope' plus the 'nameOfFunction = tempType' name-value pair
    tempScope = new Scope(globalScope, nameOfFunction, tempType) 
    type = TypeOfTerm(term, tempScope) // Calculate the type of the term 
    Unify(tempType, type)
    return type
    // After returning, the code outside will create a 'ClosedType' using the returned type and add it to the global scope.
}

Fondamentalmente il codice ottiene il tipo del termine come al solito, ma prima di unificarlo, aggiunge il nome della funzione che viene definita con un tipo fittizio nell'ambito del tipo in modo che possa essere utilizzato dall'interno in modo ricorsivo.

Modifica 2: Mi sono appena reso conto che avrei bisogno anche di tipi ricorsivi, che non ho, per definire un elenco come voglio.


Puoi essere un po 'più specifico su ciò che hai implementato esattamente? Hai implementato il calcolo lambda semplicemente tipizzato (con definizioni ricorsive) e gli hai dato polimorfismi parametrici in stile Hindley-Milner? O hai implementato il calcolo lambda polimorfo del secondo ordine?
Andrej Bauer,

Forse potrei chiedere in un modo più semplice: se prendo OCaml o SML e lo restringo a termini lambda puri e definizioni ricorsive, è di questo che stai parlando?
Andrej Bauer,

@AndrejBauer: ho modificato la domanda. Non sono sicuro di OCaml e SML, ma sono abbastanza sicuro che se prendi Haskell e lo limiti a termini lambda e permessi ricorsivi di alto livello (come let func = \x -> (func x)) ottieni quello che ho.
Juan,

1
Per migliorare la tua domanda, dai un'occhiata a questo meta post .
Juho,

Risposte:


13

Pairs

Questa codifica è la codifica della Chiesa di coppie. Tecniche simili possono codificare booleani, numeri interi, elenchi e altre strutture di dati.

x:a; y:bpair x y(a -> b -> t) -> t¬

(abt)t¬(¬a¬bt)t(ab¬t)t(ab)t
ab tpairt

pairè un costruttore per il tipo di coppia firste secondsono distruttori. (Queste sono le stesse parole utilizzate nella programmazione orientata agli oggetti; qui le parole hanno un significato correlato all'interpretazione logica dei tipi e dei termini in cui non entrerò qui.) Intuitivamente, i distruttori ti consentono di accedere a ciò che è nell'oggetto e nei costruttori aprono la strada al distruttore prendendo come argomento una funzione che applicano alle parti dell'oggetto. Questo principio può essere applicato ad altri tipi.

Le somme

La codifica della Chiesa di un'unione discriminata è essenzialmente doppia alla codifica della Chiesa di una coppia. Quando una coppia ha due parti che devono essere unite e puoi scegliere di estrarre l'una o l'altra, puoi scegliere di costruire l'unione in uno dei due modi e quando lo usi devi tener conto di entrambi. Quindi ci sono due costruttori e un solo distruttore che accetta due argomenti.

let case w = λf. λg. w f g           case : ((a->t) -> (b->t) -> t) -> (a->t) -> (b->t) -> t
  (* or simply let case w = w *)
let left x = λf. λg. f x             left : a -> ((a->t) -> (b->t) -> t)
let right y = λf. λg. g x            right : b -> ((a->t) -> (b->t) -> t)

Vorrei abbreviare il tipo (a->t) -> (b->t) -> tcome SUM(a,b)(t). Quindi i tipi di distruttori e costruttori sono:

case : SUM(a,b)(t) -> (a->t) -> (b->t) -> t
left : a -> SUM(a,b)(t)
right : b -> SUM(a,b)(t)

così

case (left x) f g → f x
case (rightt y) f g → g y

elenchi

Per un elenco, applicare nuovamente lo stesso principio. Un elenco i cui elementi hanno il tipo apuò essere creato in due modi: può essere un elenco vuoto o può essere un elemento (la testa) più un elenco (la coda). Rispetto alle coppie, c'è una piccola svolta per quanto riguarda i distruttori: non puoi avere due distruttori separati heade tailperché funzionerebbero solo su liste non vuote. È necessario un singolo distruttore, con due argomenti, uno dei quali è una funzione a 0 argomenti (ovvero un valore) per il caso zero e l'altro a una funzione a 2 argomenti per il caso contro. Funzioni come is_empty, heade tailpossono essere derivati da questo. Come nel caso delle somme, l'elenco è direttamente la sua funzione di distruttore.

let nil = λn. λc. n
let cons h t = λn. λc. c h t
let is_empty l = l true (λh. λt. false) 
let head l default = l default (λh. λt. h)
let tail l default = l default (λh. λt. t)

consconsconsTT1,,Tn

Come si suppone, se si desidera definire un tipo che contiene solo elenchi omogenei, sono necessari tipi ricorsivi. Perché? Diamo un'occhiata al tipo di un elenco. Un elenco è codificato come una funzione che accetta due argomenti: il valore da restituire su elenchi vuoti e la funzione per calcolare il valore da restituire in una cella contro. Sia ail tipo di elemento, bsia il tipo dell'elenco e csia il tipo restituito dal distruttore. Il tipo di un elenco è

a -> (a -> b -> c) -> c

Rendere omogeneo l'elenco significa che se si tratta di una cella contro, la coda deve avere lo stesso tipo dell'intero, ovvero aggiunge il vincolo

a -> (a -> b -> c) -> c = b

Il sistema di tipo Hindley-Milner può essere esteso con tali tipi ricorsivi, e in effetti i linguaggi di programmazione pratica lo fanno. I linguaggi di programmazione pratica tendono a non consentire tali equazioni "nude" e richiedono un costruttore di dati, ma questo non è un requisito intrinseco della teoria di base. La richiesta di un costruttore di dati semplifica l'inferenza del tipo e, in pratica, tende ad evitare di accettare funzioni che sono in realtà buggy ma risultano tipizzabili con un vincolo non intenzionale che causa un errore di tipo di difficile comprensione in cui viene utilizzata la funzione. Ecco perché, ad esempio, OCaml accetta tipi ricorsivi non custoditi solo con l' -rectypesopzione di compilatore non predefinita . Ecco le definizioni sopra nella sintassi OCaml, insieme a una definizione del tipo per elenchi omogenei usando la notazione pertipi ricorsivi con alias : type_expression as 'asignifica che il tipo type_expressionè unificato con la variabile 'a.

# let nil = fun n c -> n;;
val nil : 'a -> 'b -> 'a = <fun>
# let cons h t = fun n c -> c h t;;
val cons : 'a -> 'b -> 'c -> ('a -> 'b -> 'd) -> 'd = <fun>
# let is_empty l = l true (fun h t -> false);;
val is_empty : (bool -> ('a -> 'b -> bool) -> 'c) -> 'c = <fun>
# let head l default = l default (fun h t -> h);;
val head : ('a -> ('b -> 'c -> 'b) -> 'd) -> 'a -> 'd = <fun>
# let tail l default = l default (fun h t -> t);;
val tail : ('a -> ('b -> 'c -> 'c) -> 'd) -> 'a -> 'd = <fun>
# type ('a, 'b, 'c) ulist = 'c -> ('a -> 'b -> 'c) -> 'c;;
type ('a, 'b, 'c) ulist = 'c -> ('a -> 'b -> 'c) -> 'c
# is_empty (cons 1 nil);;
- : bool = false
# head (cons 1 nil) 0;;
- : int = 1
# head (tail (cons 1 (cons 2.0 nil)) nil) 0.;;
- : float = 2.

(* -rectypes is required for what follows *)
# type ('a, 'b, 'c) rlist = 'c -> ('a -> 'b -> 'c) -> 'c as 'b;;
type ('a, 'b, 'c) rlist = 'b constraint 'b = 'c -> ('a -> 'b -> 'c) -> 'c
# let rcons = (cons : 'a -> ('a, 'b, 'c) rlist -> ('a, 'b, 'c) rlist);;
val rcons :
  'a ->
  ('a, 'c -> ('a -> 'b -> 'c) -> 'c as 'b, 'c) rlist -> ('a, 'b, 'c) rlist =
  <fun>
# head (rcons 1 (rcons 2 nil)) 0;;
- : int = 1
# tail (rcons 1 (rcons 2 nil)) nil;;
- : 'a -> (int -> 'a -> 'a) -> 'a as 'a = <fun>
# rcons 1 (rcons 2.0 nil);;
Error: This expression has type
         (float, 'b -> (float -> 'a -> 'b) -> 'b as 'a, 'b) rlist = 'a
       but an expression was expected of type
         (int, 'b -> (int -> 'c -> 'b) -> 'b as 'c, 'b) rlist = 'c

Folds

Guardando questo un po 'più in generale, qual è la funzione che rappresenta la struttura dei dati?

  • nn
  • (x,y)xy
  • ini(x)ix
  • [x1,,xn]

In termini generali, la struttura dei dati è rappresentata come funzione di piegatura . Questo è un concetto generale per le strutture di dati: una funzione di piegatura è una funzione di ordine superiore che attraversa la struttura di dati. C'è un senso tecnico in cui fold è universale : tutti gli attraversamenti "generici" della struttura dei dati possono essere espressi in termini di fold. Che la struttura dei dati possa essere rappresentata come dimostra la sua funzione di piegatura: tutto ciò che devi sapere su una struttura di dati è come attraversarla, il resto è un dettaglio di implementazione.


Citi la " codifica della Chiesa " di numeri interi, coppie, somme, ma per le liste dai la codifica di Scott . Penso che potrebbe essere un po 'confuso per coloro che non hanno familiarità con le codifiche di tipi induttivi.
Stéphane Gimenez,

Quindi, fondamentalmente, il mio tipo di coppia non è in realtà un tipo di prodotto in quanto una funzione con questo tipo potrebbe semplicemente restituire te ignorare l'argomento che dovrebbe assumere ae b(che è esattamente quello che (a and b) or tsta dicendo). E sembra che avrei avuto lo stesso tipo di problemi con le somme. Inoltre, senza tipi ricorsivi non avrò un elenco omogeneo. Quindi, in poche parole, stai dicendo che dovrei aggiungere regole di somma, prodotto e tipo ricorsivo per ottenere elenchi omogenei?
Juan,

Intendevi case (right y) f g → g yalla fine della sezione Somma ?
Juan,

@ StéphaneGimenez non me ne ero reso conto. Non sono abituato a lavorare su queste codifiche in un mondo tipizzato. Puoi fornire un riferimento per la codifica Church vs la codifica Scott?
Gilles 'SO- smetti di essere malvagio' il

@JuanLuisSoldi Probabilmente hai sentito che "non c'è problema che non possa essere risolto con un ulteriore livello di riferimento indiretto". Le codifiche della chiesa codificano le strutture di dati come funzioni aggiungendo un livello di chiamata di funzione: una struttura di dati diventa una funzione di secondo ordine che si applica alla funzione per agire sulle parti. Se vuoi un tipo di elenco omogeneo, dovrai affrontare il fatto che il tipo di coda è lo stesso del tipo di tutto l'elenco. Penso che questo debba comportare una forma di ricorsione del tipo.
Gilles 'SO- smetti di essere malvagio' il

2

È possibile rappresentare i tipi di somma come tipi di prodotto con tag e valori. In questo caso, possiamo imbrogliare un po 'e usare un tag per rappresentare null o no, avendo il secondo tag che rappresenta la coppia testa / coda.

Definiamo i booleani nel solito modo:

true = λi.λe.i
false = λi.λe.e
if = λcond.λthen.λelse.(cond then else)

Un elenco è quindi una coppia con il primo elemento come booleano e il secondo elemento come coppia testa / coda. Alcune funzioni di base dell'elenco:

isNull = λl.(first l)
null = pair false false     --The second element doesn't matter in this case
cons = λh.λt.(pair true (pair h t ))
head = λl.(fst (snd l))   --This is a partial function
tail = λl.(snd (snd l))   --This is a partial function  

map = λf.λl.(if (isNull l)
                 null 
                 (cons (f (head l)) (map f (tail l) ) ) 

Ma questo non mi darebbe un elenco omogeneo, è corretto?
Juan,
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.