Teoria dei tipi dipendenti e funzioni dei tipi "arbitrarie"
La mia prima risposta a questa domanda era alta sui concetti e scarsa sui dettagli e riflessa sulla domanda secondaria: "che cosa sta succedendo?"; questa risposta sarà la stessa ma focalizzata sulla domanda secondaria: "possiamo ottenere funzioni di tipo arbitrario?".
Un'estensione alle operazioni algebriche di somma e prodotto sono i cosiddetti "grandi operatori", che rappresentano la somma e il prodotto di una sequenza (o più in generale, la somma e il prodotto di una funzione su un dominio) normalmente scritti Σ
e Π
rispettivamente. Vedi la Sigma Notation .
Quindi la somma
a₀ + a₁X + a₂X² + ...
potrebbe essere scritto
Σ[i ∈ ℕ]aᵢXⁱ
dove a
c'è una sequenza di numeri reali, per esempio. Il prodotto verrebbe rappresentato in modo simile con Π
invece di Σ
.
Quando guardi da lontano, questo tipo di espressione assomiglia molto a una funzione 'arbitraria' X
; siamo ovviamente limitati a serie espressibili e alle loro funzioni analitiche associate. È questo un candidato per una rappresentazione in una teoria dei tipi? Decisamente!
La classe delle teorie dei tipi che hanno rappresentazioni immediate di queste espressioni è la classe delle teorie dei tipi "dipendenti": teorie con tipi dipendenti. Naturalmente abbiamo termini che dipendono da termini e in lingue come Haskell con funzioni e quantificazione dei tipi, termini e tipi a seconda dei tipi. In un'impostazione dipendente, abbiamo anche tipi a seconda dei termini. Haskell non è un linguaggio tipicamente dipendente, sebbene molte funzioni di tipi dipendenti possano essere simulate torturando un po 'il linguaggio .
Curry-Howard e tipi dipendenti
L'isomorfismo di Curry-Howard iniziò la vita come un'osservazione che i termini e le regole di giudizio di tipo del calcolo lambda semplicemente tipizzato corrispondono esattamente alla deduzione naturale (come formulata da Gentzen) applicata alla logica proposizionale intuizionistica, con i tipi che prendono il posto delle proposizioni e termini che sostituiscono le prove, nonostante i due siano stati inventati / scoperti indipendentemente. Da allora, è stata un'enorme fonte di ispirazione per i teorici dei tipi. Una delle cose più ovvie da considerare è se e come questa corrispondenza per la logica proposizionale possa essere estesa a predicazioni o logiche di ordine superiore. Le teorie del tipo dipendente inizialmente sorsero da questa via di esplorazione.
Per un'introduzione all'isomorfismo di Curry-Howard per il calcolo lambda tipicamente semplice, vedere qui . Ad esempio, se vogliamo dimostrare A ∧ B
dobbiamo dimostrare A
e dimostrare B
; una dimostrazione combinata è semplicemente una coppia di dimostrazioni: una per ciascun congiunto.
In deduzione naturale:
Γ ⊢ A Γ ⊢ B
Γ ⊢ A ∧ B
e nel calcolo lambda semplicemente digitato:
Γ ⊢ a : A Γ ⊢ b : B
Γ ⊢ (a, b) : A × B
Corrispondenze simili esistono per ∨
tipi e somma, →
tipi di funzione e varie regole di eliminazione.
Una proposizione non dimostrabile (intuizionisticamente falsa) corrisponde a un tipo disabitato.
Tenendo presente l'analogia dei tipi come proposizioni logiche, possiamo iniziare a considerare come modellare i predicati nel mondo dei tipi. Ci sono molti modi in cui questo è stato formalizzato (vedi questa introduzione alla teoria del tipo intuitivo di Martin-Löf per uno standard ampiamente usato) ma l'approccio astratto di solito osserva che un predicato è come una proposizione con variabili a termine libero, o, in alternativa, una funzione che prende in considerazione le proposizioni. Se permettiamo alle espressioni di tipo di contenere termini, allora un trattamento nello stile del calcolo lambda si presenta immediatamente come una possibilità!
Considerando solo prove costruttive, di cosa costituisce una prova ∀x ∈ X.P(x)
? Possiamo considerarlo come una funzione di prova, prendendo termini ( x
) come prove delle loro proposizioni corrispondenti ( P(x)
). Così i membri (prove) del tipo (proposizione) ∀x : X.P(x)
sono 'funzioni dipendenti', che per ogni x
a X
danno un termine di tipo P(x)
.
Che dire ∃x ∈ X.P(x)
? Abbiamo bisogno di ogni membro del X
, x
insieme a una prova P(x)
. Quindi i membri (prove) del tipo (proposizione) ∃x : X.P(x)
sono "coppie dipendenti": un termine distinto x
in X
, insieme a un termine di tipo P(x)
.
Notazione: userò
∀x ∈ X...
per dichiarazioni effettive sui membri della classe X
e
∀x : X...
per espressioni di tipo corrispondenti alla quantificazione universale sul tipo X
. Allo stesso modo per ∃
.
Considerazioni combinatorie: prodotti e somme
Oltre alla corrispondenza di tipi Curry-Howard con proposizioni, abbiamo la corrispondenza combinatoria di tipi algebrici con numeri e funzioni, che è il punto principale di questa domanda. Fortunatamente, questo può essere esteso ai tipi dipendenti descritti sopra!
Userò la notazione del modulo
|A|
rappresentare la "dimensione" di un tipo A
, rendere esplicita la corrispondenza delineata nella domanda, tra tipi e numeri. Si noti che questo è un concetto al di fuori della teoria; Non pretendo che ci sia bisogno di tale operatore all'interno della lingua.
Contiamo i possibili membri (completamente ridotti, canonici) di tipo
∀x : X.P(x)
che è il tipo di funzioni dipendenti che portano termini x
di tipo X
a termini di tipo P(x)
. Ciascuna di tali funzioni deve avere un output per ogni termine di X
e questo output deve essere di un tipo particolare. Per ciascuno x
di essi X
, quindi, ciò fornisce |P(x)|
"scelte" di output.
La battuta finale è
|∀x : X.P(x)| = Π[x : X]|P(x)|
che ovviamente non ha molto senso se lo X
è IO ()
, ma è applicabile ai tipi algebrici.
Allo stesso modo, un termine di tipo
∃x : X.P(x)
è il tipo di coppie (x, p)
con p : P(x)
, quindi dato qualsiasi x
in X
possiamo costruire una coppia appropriata con qualsiasi membro di P(x)
, dando |P(x)|
"scelte".
Quindi,
|∃x : X.P(x)| = Σ[x : X]|P(x)|
con gli stessi avvertimenti.
Ciò giustifica la notazione comune per i tipi dipendenti nelle teorie che usano i simboli Π
e Σ
, e in effetti molte teorie confondono la distinzione tra "per tutti" e "prodotto" e tra "c'è" e "somma", a causa delle corrispondenze sopra menzionate.
Ci stiamo avvicinando!
Vettori: rappresentano le tuple dipendenti
Possiamo ora codificare espressioni numeriche come
Σ[n ∈ ℕ]Xⁿ
come espressioni di tipo?
Non proprio. Mentre possiamo considerare in modo informale il significato di espressioni come Xⁿ
in Haskell, dove X
c'è un tipo e n
un numero naturale, è un abuso della notazione; questa è un'espressione di tipo contenente un numero: chiaramente non un'espressione valida.
D'altra parte, con i tipi dipendenti nell'immagine, i tipi contenenti numeri è proprio il punto; in effetti, le tuple dipendenti o "vettori" sono un esempio molto comunemente citato di come i tipi dipendenti possono fornire una sicurezza pragmatica a livello di tipo per operazioni come l'accesso all'elenco . Un vettore è solo un elenco con le informazioni a livello di tipo relative alla sua lunghezza: esattamente ciò che stiamo cercando per espressioni di tipo come Xⁿ
.
Per la durata di questa risposta, lascia
Vec X n
essere il tipo di lunghezza- n
vettori di X
valori di tipo.
Tecnicamente n
qui c'è, piuttosto che un numero naturale reale , una rappresentazione nel sistema di un numero naturale. Possiamo rappresentare i numeri naturali ( Nat
) in stile Peano come zero ( 0
) o il successore ( S
) di un altro numero naturale, e per n ∈ ℕ
scrivere scrivo ˻n˼
per indicare il termine in Nat
cui rappresenta n
. Ad esempio, lo ˻3˼
è S (S (S 0))
.
Poi abbiamo
|Vec X ˻n˼| = |X|ⁿ
per qualsiasi n ∈ ℕ
.
Tipi nativi: promozione di ℕ termini per i tipi
Ora possiamo codificare espressioni come
Σ[n ∈ ℕ]Xⁿ
come tipi. Questa particolare espressione darebbe origine a un tipo che è ovviamente isomorfo al tipo di elenchi di X
, come identificato nella domanda. (Non solo, ma da un punto di vista teorico di categoria, la funzione di tipo - che è un funzione - che porta X
al tipo sopra è naturalmente isomorfa per il funzione di lista).
Un ultimo pezzo del puzzle per funzioni 'arbitrarie' è come codificare, per
f : ℕ → ℕ
espressioni come
Σ[n ∈ ℕ]f(n)Xⁿ
in modo che possiamo applicare coefficienti arbitrari a una serie di potenze.
Comprendiamo già la corrispondenza dei tipi algebrici con i numeri, permettendoci di mappare dai tipi ai numeri e digitare le funzioni alle funzioni numeriche. Possiamo anche andare dall'altra parte! - prendendo un numero naturale, c'è ovviamente un tipo algebrico definibile con quel numero di membri a termine, indipendentemente dal fatto che abbiamo o meno tipi dipendenti. Possiamo facilmente dimostrarlo al di fuori della teoria dei tipi per induzione. Ciò di cui abbiamo bisogno è un modo per mappare da numeri naturali a tipi, all'interno del sistema.
Una piacevole realizzazione è che, una volta che abbiamo tipi dipendenti, la prova per induzione e la costruzione per ricorsione diventano intimamente simili - in effetti sono la stessa cosa in molte teorie. Dal momento che possiamo dimostrare per induzione che esistono tipi che soddisfano i nostri bisogni, non dovremmo essere in grado di costruirli?
Esistono diversi modi per rappresentare i tipi a livello di termine. Userò qui una notazione immaginaria Haskellish con *
per l'universo dei tipi, di per sé considerato un tipo in un ambiente dipendente. 1
Allo stesso modo, ci sono anche almeno tanti modi per ℕ
annotare '-eliminazione' quante sono le teorie del tipo dipendente. Userò una notazione di corrispondenza del modello Haskellish.
Abbiamo bisogno di una mappatura, α
da Nat
a *
, con la proprietà
∀n ∈ ℕ.|α ˻n˼| = n.
È sufficiente la seguente pseudodefinizione.
data Zero -- empty type
data Successor a = Z | Suc a -- Successor ≅ Maybe
α : Nat -> *
α 0 = Zero
α (S n) = Successor (α n)
Quindi vediamo che l'azione di α
rispecchia il comportamento del successore S
, rendendolo una specie di omomorfismo. Successor
è una funzione di tipo che "aggiunge uno" al numero di membri di un tipo; vale a dire, |Successor a| = 1 + |a|
per chiunque abbia a
una dimensione definita.
Ad esempio α ˻4˼
(che è α (S (S (S (S 0))))
), è
Successor (Successor (Successor (Successor Zero)))
e i termini di questo tipo sono
Z
Suc Z
Suc (Suc Z)
Suc (Suc (Suc Z))
dandoci esattamente quattro elementi: |α ˻4˼| = 4
.
Allo stesso modo, per chiunque n ∈ ℕ
, abbiamo
|α ˻n˼| = n
come richiesto.
- Molte teorie richiedono che i membri di
*
siano meri rappresentanti di tipi e un'operazione viene fornita come una mappatura esplicita dai termini di tipo *
ai loro tipi associati. Altre teorie consentono agli stessi tipi letterali di essere entità a livello di termine.
Funzioni "arbitrarie"?
Ora abbiamo l'apparato per esprimere una serie di potenze completamente generali come un tipo!
La serie
Σ[n ∈ ℕ]f(n)Xⁿ
diventa il tipo
∃n : Nat.α (˻f˼ n) × (Vec X n)
dov'è ˻f˼ : Nat → Nat
una rappresentazione adatta all'interno del linguaggio della funzione f
. Possiamo vedere questo come segue.
|∃n : Nat.α (˻f˼ n) × (Vec X n)|
= Σ[n : Nat]|α (˻f˼ n) × (Vec X n)| (property of ∃ types)
= Σ[n ∈ ℕ]|α (˻f˼ ˻n˼) × (Vec X ˻n˼)| (switching Nat for ℕ)
= Σ[n ∈ ℕ]|α ˻f(n)˼ × (Vec X ˻n˼)| (applying ˻f˼ to ˻n˼)
= Σ[n ∈ ℕ]|α ˻f(n)˼||Vec X ˻n˼| (splitting product)
= Σ[n ∈ ℕ]f(n)|X|ⁿ (properties of α and Vec)
Quanto è 'arbitrario' questo? Con questo metodo non siamo limitati solo ai coefficienti interi, ma ai numeri naturali. A parte questo, f
può essere qualsiasi cosa, dato un linguaggio Turing completo con tipi dipendenti, possiamo rappresentare qualsiasi funzione analitica con coefficienti numerici naturali.
Non ho studiato l'interazione di questo con, ad esempio, il caso fornito nella domanda List X ≅ 1/(1 - X)
o quale possibile senso potrebbero avere "tipi" negativi e non interi in questo contesto.
Speriamo che questa risposta spinga in qualche modo a esplorare fino a che punto possiamo andare con funzioni di tipo arbitrario.