Qual è una breve ma completa spiegazione di un sistema di tipo puro / dipendente?


32

Se qualcosa è semplice, allora dovrebbe essere completamente spiegabile con poche parole. Questo può essere fatto per il calcolo λ:

Il calcolo λ è una grammatica sintattica (fondamentalmente una struttura) con una regola di riduzione (il che significa che una procedura di ricerca / sostituzione viene ripetutamente applicata ad ogni occorrenza di un modello specifico fino a quando non esiste tale modello).

Grammatica:

Term = (Term Term) | (λ Var . Term) | Var

Regola di riduzione:

((λ var body) term) -> SUBS(body,var,term)
    where `SUBS` replaces all occurrences of `var`
    by `term` in `body`, avoiding name capture.

Esempi:

(λ a . a)                             -> (λ a a)
((λ a . (λ b . (b a))) (λ x . x))     -> (λ b . (b (λ x x)))
((λ a . (a a)) (λ x . x))             -> (λ x . x)
((λ a . (λ b . ((b a) a))) (λ x . x)) -> (λ b . ((b (λ x . x)) (λ x . x)))
((λ x . (x x)) (λ x . (x x)))         -> never halts

Sebbene in qualche modo informale, si potrebbe sostenere che questo è abbastanza informativo per un umano normale per capire il calcolo λ nel suo insieme - e ci vogliono 22 linee di markdown. Sto cercando di capire i sistemi di tipo puro / dipendente usati da Idris / Agda e progetti simili, ma la spiegazione più breve che ho potuto trovare è stata Semplicemente facile - un ottimo documento, ma che sembra assumere molte conoscenze precedenti (Haskell, induttivo definizioni) che non ho. Penso che qualcosa di più breve, meno ricco potrebbe eliminare alcune di queste barriere. Così,

È possibile fornire una breve e completa spiegazione dei sistemi di tipo puro / dipendente, nello stesso formato in cui ho presentato il calcolo λ sopra?


4
Le regole dei sistemi di tipo puro sono molto brevi. Semplicemente Easy riguarda l' implementazione di tipi dipendenti.

2
Quindi non è "ostile" nel senso di offensivo, ma nel senso che pensi che sto chiedendo molto per non mostrare abbastanza sforzo nel trovare la risposta da solo? In tal caso, sono d'accordo che questa domanda potrebbe richiedere molto, quindi forse è solo un male. Ma c'è anche un grande sforzo dietro, pensi che dovrei modificare nei miei tentativi?
MaiaVictor,

3
Anch'io sono offeso per conto dei miei coautori che hanno scritto il testo di "Un'implementazione tutorial di un calcolo Lambda tipicamente dipendente", che ha sostituito "Semplicemente facile" come titolo provvisorio. Ho scritto il kernel del codice, che è un typechecker in <100 righe di Haskell.

2
Quindi mi sono sicuramente espresso male. Adoro il documento "Semplicemente facile" e lo leggo in ogni pausa da qualche giorno fa - è l'unica cosa al mondo che mi ha dato una sensazione parziale sto iniziando a capire l'argomento (e credo di aver provato) . Ma penso che sia rivolto a un pubblico con più conoscenza di me e questo potrebbe essere il motivo per cui ho ancora problemi a farne parte. Niente a che vedere con la qualità della carta, ma i miei limiti.
MaiaVictor,

1
@pigworker e il codice è la mia parte preferita, proprio perché (in relazione alla spiegazione inglese) è una spiegazione molto più breve, ma completa, del tutto, come ho chiesto qui. Hai una copia del codice che posso scaricare?
MaiaVictor,

Risposte:


7

disconoscimento

Questo è molto informale, come da lei richiesto.

La grammatica

In un linguaggio tipicamente dipendente abbiamo un raccoglitore sia a livello di tipo che a livello di valore:

Term = * | (∀ (Var : Term). Term) | (Term Term) | (λ Var. Term) | Var

Termine ben digitato è un termine con tipo allegato, scriveremo t ∈ σo

σ
t

per indicare che il termine tha tipo σ.

Regole di battitura

Per semplicità, ciò è necessario in λ v. t ∈ ∀ (v : σ). τentrambi λe associamo la stessa variabile ( vin questo caso).

Regole:

t ∈ σ is well-formed if σ ∈ * and t is in normal form (0)

*            ∈ *                                                 (1)
∀ (v : σ). τ ∈ *             -: σ ∈ *, τ ∈ *                     (2)
λ v. t       ∈ ∀ (v : σ). τ  -: t ∈ τ                            (3)
f x          ∈ SUBS(τ, v, x) -: f ∈ ∀ (v : σ). τ, x ∈ σ          (4)
v            ∈ σ             -: v was introduced by ∀ (v : σ). τ (5)

Quindi, *è "il tipo di tutti i tipi" (1), i tipi di forme dai tipi (2), le astrazioni lambda hanno tipi-pi (3) e se vviene introdotto da ∀ (v : σ). τ, allora vha tipo σ(5).

"in forma normale" significa che eseguiamo quante più riduzioni possibili usando la regola di riduzione:

"La" regola di riduzione

(λ v. b ∈ ∀ (v : σ). τ) (t ∈ σ) ~> SUBS(b, v, t) ∈ SUBS(τ, v, t)
    where `SUBS` replaces all occurrences of `v`
    by `t` in `τ` and `b`, avoiding name capture.

O nella sintassi bidimensionale dove

σ
t

significa t ∈ σ:

(∀ (v : σ). τ) σ    SUBS(τ, v, t)
                 ~>
(λ  v     . b) t    SUBS(b, v, t)

È possibile applicare un'astrazione lambda a un termine solo quando il termine ha lo stesso tipo della variabile nel quantificatore forall associato. Quindi riduciamo sia l'astrazione lambda che il quantificatore forall allo stesso modo del calcolo lambda puro prima. Dopo aver sottratto la parte del livello di valore, otteniamo la (4) regola di battitura.

Un esempio

Ecco l'operatore dell'applicazione funzione:

∀ (A : *) (B : A -> *) (f : ∀ (y : A). B y) (x : A). B x
λ  A       B            f                    x     . f x

(abbreviamo ∀ (x : σ). τa σ -> τse τnon fa menzione x)

fritorni B yper qualsiasi fornito ydi tipo A. Applichiamo fa x, che è del tipo giusto A, e sostituire yper xla dopo ., quindi f x ∈ SUBS(B y, y, x)~> f x ∈ B x.

Abbreviamo ora l'operatore dell'applicazione funzione come appe appliciamolo a se stesso:

∀ (A : *) (B : A -> *). ?
λ  A       B          . app ? ? (app A B)

Posto ?per i termini che dobbiamo fornire. Innanzitutto introduciamo e istanziamo esplicitamente Ae B:

∀ (f : ∀ (y : A). B y) (x : A). B x
app A B

Ora dobbiamo unificare ciò che abbiamo

∀ (f : ∀ (y : A). B y) (x : A). B x

che è lo stesso di

(∀ (y : A). B y) -> ∀ (x : A). B x

e ciò che app ? ?riceve

∀ (x : A'). B' x

Questo risulta in

A' ~ ∀ (y : A). B y
B' ~ λ _. ∀ (x : A). B x -- B' ignores its argument

(vedi anche Cos'è la predicatività? )

La nostra espressione (dopo qualche rinominazione) diventa

∀ (A : *) (B : A -> *). ?
λ  A       B          . app (∀ (x : A). B x) (λ _. ∀ (x : A). B x) (app A B)

Dal momento che per qualsiasi A, Be f(dove f ∈ ∀ (y : A). B y)

∀ (y : A). B y
app A B f

possiamo istanziare Ae Bottenere (per chiunque abbia fil tipo appropriato)

∀ (y : ∀ (x : A). B x). ∀ (x : A). B x
app (∀ (x : A). B x) (λ _. ∀ (x : A). B x) f

e la firma del tipo equivale a (∀ (x : A). B x) -> ∀ (x : A). B x.

L'intera espressione è

∀ (A : *) (B : A -> *). (∀ (x : A). B x) -> ∀ (x : A). B x
λ  A       B          . app (∀ (x : A). B x) (λ _. ∀ (x : A). B x) (app A B)

ie

∀ (A : *) (B : A -> *) (f : ∀ (x : A). B x) (x : A). B x
λ  A       B            f                    x     .
    app (∀ (x : A). B x) (λ _. ∀ (x : A). B x) (app A B) f x

che dopo tutto le riduzioni a livello di valore restituiscono lo stesso app.

Così, mentre richiede solo pochi passi nel lambda calcolo puro per ottenere appda app app, in un ambiente tipizzato (e soprattutto un dipendente digitato) abbiamo anche bisogno di preoccuparsi unificazione e le cose diventano più complesse, anche con una certa comodità inconsitent ( * ∈ *).

Tipo di controllo

  • Se tè *quindi t ∈ *di (1)
  • Se tè ∀ (x : σ) τ, σ ∈? *, τ ∈? *(si veda la nota a proposito ∈?qui di seguito) e poi t ∈ *da (2)
  • Se tè f x, f ∈ ∀ (v : σ) τper alcuni σe τ, x ∈? σquindi t ∈ SUBS(τ, v, x)per (4)
  • Se tè una variabile v, è vstato introdotto da ∀ (v : σ). τallora t ∈ σda (5)

Queste sono tutte regole di inferenza, ma non possiamo fare lo stesso per lambdas (l'inferenza di tipo è indecidibile per tipi dipendenti). Quindi per lambdas controlliamo ( t ∈? σ) anziché inferire:

  • Se tè λ v. be verificato contro ∀ (v : σ) τ, b ∈? τallorat ∈ ∀ (v : σ) τ
  • Se tè qualcos'altro e verificato, σquindi dedurre il tipo di tutilizzo della funzione sopra e verificare se lo èσ

Il controllo dell'uguaglianza per i tipi richiede che siano in forme normali, quindi per decidere se tha tipo σcontrolliamo prima che σabbia tipo *. Se è così, allora σè normalizzabile (il paradosso di modulo Girard) e si normalizza (quindi σdiventa ben formato da (0)). SUBSnormalizza anche le espressioni da conservare (0).

Questo si chiama controllo bidirezionale del tipo. Con esso non abbiamo bisogno di annotare ogni lambda con un tipo: se nel f xtipo di fè noto, allora xviene verificato il tipo di argomento fricevuto invece di essere dedotto e confrontato per l'uguaglianza (che è anche meno efficiente). Ma se fè un lambda, richiede un'annotazione esplicita del tipo (le annotazioni sono omesse nella grammatica e ovunque, puoi aggiungere Ann Term Termo λ' (σ : Term) (v : Var)ai costruttori).

Inoltre, dai un'occhiata al più semplice, più facile! post sul blog.


1
Secondo "Più semplice, più facile".

La prima regola di riduzione su forall sembra strana. A differenza delle lambda, le forcelle non dovrebbero essere applicate in modo ben tipizzato (giusto?).

@chi, non capisco cosa stai dicendo. Forse la mia notazione è cattiva: la regola di riduzione dice (λ v. b ∈ ∀ (v : σ). τ) (t ∈ σ)~> SUBS(b, v, t) ∈ SUBS(τ, v, t).
user3237465

1
Trovo la notazione fuorviante. Sembra che tu abbia avuto due regole: una per l'assurdità (∀ (v : σ). τ) t ~> ...e un'altra per il significativo (λ v. b) t ~> .... Vorrei rimuovere il primo e trasformarlo in un commento qui sotto.

1
La regola (1) contiene la sua conclusione come premessa. Puoi confrontare la semplicità del tuo sistema con la versione bidirezionale solo una volta che hai un sistema che funziona. Puoi dire di mantenere tutto normalizzato, ma le tue regole no.

24

Proviamo. Non mi preoccuperò del paradosso di Girard, perché distrae dalle idee centrali. Dovrò presentare alcuni meccanismi di presentazione su giudizi, derivazioni e simili.

Grammatica

Termine :: = (Elim) | * | (Var: Term) → Term | λVar↦Term

Elim :: = Term: Term | Var | Termine Elim

La grammatica ha due forme reciprocamente definite, "termini" che sono la nozione generale di cosa (i tipi sono cose, i valori sono cose), inclusi * (il tipo di tipi), tipi di funzione dipendenti e lambda-astrazioni, ma anche incorporamento " eliminazioni "(ovvero" usi "anziché" costruzioni "), che sono applicazioni nidificate in cui la cosa alla fine nella posizione della funzione è una variabile o un termine annotato con il suo tipo.

Regole di riduzione

(λy↦t: (x: S) → T) s ↝ t [s: S / y]: T [s: S / x]

(t: T) ↝ t

L'operazione di sostituzione t [e / x] sostituisce ogni occorrenza della variabile x con l'eliminazione e, evitando l'acquisizione del nome. Per formare un'applicazione che può ridurre, un termine lambda deve essere annotato dal suo tipo per effettuare un'eliminazione . L'annotazione del tipo conferisce all'astrazione lambda una sorta di "reattività", consentendo all'applicazione di procedere. Una volta raggiunto il punto in cui non si verificano più applicazioni e la t: T attiva viene incorporata nuovamente nella sintassi del termine, è possibile eliminare l'annotazione del tipo.

Estendiamo la relazione di riduzione by con la chiusura strutturale: le regole si applicano ovunque all'interno dei termini ed eliminazioni che puoi trovare qualcosa che corrisponda al lato sinistro. Scrivi ↝ * per la chiusura riflessiva-transitiva (0 o più fasi) di ↝. Il sistema di riduzione risultante è confluente in questo senso:

Se s ↝ * p e s ↝ * q, allora esiste qualche r tale che p ↝ * r e q ↝ * r.

contesti

Contesto :: = | Contesto, Var: Term

I contesti sono sequenze che assegnano i tipi alle variabili, crescendo a destra, che noi consideriamo l'estremità "locale", che ci parla delle variabili associate più di recente. Una proprietà importante dei contesti è che è sempre possibile scegliere una variabile non già utilizzata nel contesto. Manteniamo l'invariante che le variabili attribuite ai tipi nel contesto sono distinte.

Sentenze

Giudizio :: = Contesto ⊢ Il termine ha termine | Contesto ⊢ Elim è termine

Questa è la grammatica dei giudizi, ma come leggerli? Tanto per cominciare, ⊢ è il tradizionale simbolo del "tornello", che separa le ipotesi dalle conclusioni: puoi leggerlo in modo informale come "dice".

G ⊢ T ha t

significa che dato il contesto G, tipo T ammette il termine t;

G ⊢ e è S

significa che dato il contesto G, l'eliminazione e viene data di tipo S.

I giudizi hanno una struttura interessante: zero o più input , uno o più soggetti , zero o più output .

INPUTS                   SUBJECT        OUTPUTS
Context |- Term   has    Term
Context |-               Elim      is   Term

Cioè, dobbiamo proporre in anticipo i tipi di termini e controllarli , ma sintetizziamo i tipi di eliminazioni.

Regole di battitura

Li presento in uno stile vagamente Prolog, scrivendo J -: P1; ...; Pn per indicare che la sentenza J vale se valgono anche le premesse da P1 a Pn. Una premessa sarà un altro giudizio o un reclamo sulla riduzione.

condizioni

G ⊢ T ha t -: T ↝ R; G ⊢ R ha t

G ⊢ * ha *

G ⊢ * ha (x: S) → T -: G ⊢ * ha S; G, z: S! - * ha T [z / x]

G ⊢ (x: S) → T ha λy↦t -: G, z: S ⊢ T [z / x] ha t [z / y]

G ⊢ T ha (e) -: G ⊢ e è T

eliminazioni

G ⊢ e è R -: G ⊢ e è S; S ↝ R

G, x: S, G '⊢ x è S

G ⊢ fs è T [s: S / x] -: G ⊢ f è (x: S) → T; G ⊢ S ha s

E questo è tutto!

Solo due regole non sono dirette dalla sintassi: la regola che dice "puoi ridurre un tipo prima di usarlo per controllare un termine" e la regola che dice "puoi ridurre un tipo dopo averlo sintetizzato da un'eliminazione". Una strategia praticabile è quella di ridurre i tipi fino a quando non hai esposto il costruttore più in alto.

Questo sistema non si sta fortemente normalizzando (a causa del paradosso di Girard, che è un paradosso in stile bugiardo di autoreferenziazione), ma può essere reso fortemente normalizzante suddividendo * in "livelli di universo" in cui tutti i valori che coinvolgono tipi a livelli inferiori stessi hanno tipi a livelli più alti, impedendo l'autoreferenzialità.

Questo sistema, tuttavia, ha la proprietà di preservare il tipo, in questo senso.

Se G ⊢ T ha te G ↝ * D e T ↝ * R e t ↝ r, allora D ⊢ R ha r.

Se G ⊢ e è S e G ↝ * D ed e ↝ f, allora esiste R tale che S ↝ * R e D ⊢ f è R.

I contesti possono essere calcolati consentendo il calcolo dei termini in essi contenuti. Cioè, se un giudizio è valido ora, puoi calcolare i suoi input quanto vuoi e il suo soggetto un passo, e quindi sarà possibile calcolare i suoi output in qualche modo per assicurarsi che il giudizio risultante rimanga valido. La dimostrazione è una semplice induzione sulle derivazioni tipografiche, data la confluenza di -> *.

Naturalmente, ho presentato solo il nucleo funzionale qui, ma le estensioni possono essere abbastanza modulari. Ecco le coppie.

Termine :: = ... | (x: S) * T | s, t

Elim :: = ... | e.head | e.tail

(s, t: (x: S) * T) .head ↝ s: S

(s, t: (x: S) * T) .tail ↝ t: T [s: S / x]

G ⊢ * ha (x: S) * T -: G ⊢ * ha S; G, z: S ⊢ * ha T [z / x]

G ⊢ (x: S) * T ha s, t -: G ⊢ S ha s; G ⊢ T [s: S / x] ha t

G ⊢ e.head è S -: G ⊢ e è (x: S) * T

G ⊢ e.tail è T [e.head / x] -: G ⊢ e è (x: S) * T


1
G, x:S, G' ⊢ x is S -: G' ⊬ x?
user3237465

1
@ user3237465 No. Grazie! Fisso. (Quando stavo sostituendo i tornelli di arte ascii con tornelli html (rendendoli così invisibili sul mio telefono; scusate se succede altrove) Mi mancava quello.)

1
Oh, ho pensato che stavi solo sottolineando l'errore di battitura. La regola dice che, per ogni variabile nel contesto, sintetizziamo il tipo che il contesto le assegna. Quando ho introdotto i contesti, ho detto "Manteniamo l'invariante che le variabili attribuite ai tipi nel contesto sono distinte". così l'ombreggiatura è vietata. Vedrai che ogni volta che le regole estendono il contesto, scelgono sempre una nuova "z" che crea un'istanza di qualunque legante sotto cui ci stiamo muovendo. L'ombra è un anatema. Se hai il contesto x: *, x: x, il tipo di x più locale non è più ok perché è x fuori ambito.

1
Volevo solo che tu e gli altri soccorritori sapessimo che torno a questo thread ogni interruzione dal lavoro. Voglio davvero imparare questo, e per la prima volta sono caduto come se avessi effettivamente la maggior parte. Il prossimo passo sarà implementare e scrivere alcuni programmi. Sono lieto di poter vivere in un'epoca in cui informazioni su argomenti così meravigliosi sono disponibili in tutto il mondo a qualcuno come me, e questo è tutto grazie a geni come te che dedicano un po 'di tempo della loro vita a diffondere tale conoscenza, per gratuito, su internet. Scusami ancora per aver formulato male la mia domanda, e grazie!
MaiaVictor,

1
@cody Sì, non c'è espansione. Per capire perché non è necessario, si noti che le due regole di calcolo consentono di distribuire la strategia in cui si normalizzano completamente i tipi prima di controllare i termini e si normalizzano anche i tipi immediatamente dopo averli sintetizzati dalle eliminazioni. Quindi nella regola in cui i tipi devono corrispondere, sono già normalizzati, quindi uguali al naso se i tipi "originali" controllati e sintetizzati fossero convertibili. Nel frattempo, limitare i controlli di uguaglianza a quel posto va bene solo per questo fatto: se T è convertibile in un tipo canonico, si riduce a un tipo canonico.
Pigworker,

8

La corrispondenza Curry-Howard afferma che esiste una corrispondenza sistematica tra sistemi di tipo e sistemi di prova in logica. Avendo una visione incentrata sul programmatore di questo, potresti rifonderlo in questo modo:

  • I sistemi a prova logica sono linguaggi di programmazione.
  • Queste lingue sono tipizzate staticamente.
  • La responsabilità del sistema di tipi in tale linguaggio è di vietare programmi che costruiscano prove non fondate.

Visto da questo angolo:

  • Il calcolo lambda non tipizzato che riassumi non ha un sistema di tipo significativo, quindi un sistema di prova basato su di esso sarebbe privo di fondamento.
  • Il calcolo lambda semplicemente digitato è un linguaggio di programmazione che ha tutti i tipi necessari per costruire prove sonore nella logica sentenziale ("if / then", "and", "or", "not"). Ma i suoi tipi non sono abbastanza buoni per verificare le prove che coinvolgono quantificatori ("per tutte x, ..."; "esiste una x tale che ...").
  • Il calcolo lambda tipicamente dipendente ha tipi e regole che supportano la logica sentenziale e quantificatori del primo ordine (quantificazione su valori).

Ecco le regole della detrazione naturale per la logica del primo ordine, usando un diagramma della voce di Wikipedia sulla detrazione naturale . Queste sono fondamentalmente anche le regole di un calcolo lambda minimamente tipizzato.

Detrazione naturale del primo ordine

Nota che le regole hanno termini lambda in esse. Questi possono essere letti come i programmi che costruiscono le prove delle frasi rappresentate dai loro tipi (o più succintamente, diciamo solo che i programmi sono prove ). Regole di riduzione simili fornite possono essere applicate a questi termini lambda.


Perché ci importa di questo? Bene, prima di tutto, perché le prove possono rivelarsi uno strumento utile nella programmazione e avere un linguaggio che può funzionare con le prove come oggetti di prima classe apre molte strade. Ad esempio, se la tua funzione ha una condizione preliminare, invece di scriverla come commento, puoi effettivamente richiederne una prova come argomento.

In secondo luogo, poiché le macchine del sistema di tipi necessarie per gestire i quantificatori possono avere altri usi in un contesto di programmazione. In particolare, i linguaggi tipizzati in modo dipendente gestiscono i quantificatori universali ("per tutti x, ...") utilizzando un concetto chiamato tipi di funzione dipendenti, una funzione in cui il tipo statico del risultato può dipendere dal valore di runtime dell'argomento.

Per fornire un'applicazione molto pedonale, scrivo sempre codice che deve leggere i file Avro costituiti da record con struttura uniforme, tutti condividono lo stesso set di nomi e tipi di campi. Ciò richiede che:

  1. Hardcode la struttura dei record nel programma come tipo di record.
    • Vantaggi: il codice è più semplice e il compilatore può rilevare errori nel mio codice
    • Svantaggio: il programma è hardcoded per leggere i file che concordano con il tipo di record.
  2. Leggi lo schema dei dati in fase di esecuzione, rappresentalo genericamente come una struttura di dati e utilizzalo per elaborare i record genericamente
    • Vantaggi: il mio programma non è codificato su un solo tipo di file
    • Svantaggi: il compilatore non può rilevare tanti errori.

Come puoi vedere nella pagina tutorial di Avro Java , ti mostrano come usare la libreria secondo entrambi questi approcci.

Con i tipi di funzioni dipendenti puoi avere la tua torta e mangiarla, al costo di un sistema di tipi più complesso. È possibile scrivere una funzione che legge un file Avro, estrae lo schema e restituisce il contenuto del file come flusso di record il cui tipo statico dipende dallo schema memorizzato nel file . Il compilatore sarebbe in grado di rilevare errori laddove, ad esempio, ho provato ad accedere a un campo denominato che potrebbe non esistere nei record dei file che elaborerà in fase di esecuzione. Eh dolce?


1
Costruire i tipi in fase di esecuzione nel modo che hai citato è qualcosa di veramente interessante a cui non ho pensato. Piuttosto dolce, davvero! Grazie per la risposta perspicace.
MaiaVictor,
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.