Odvisni tipi#
Pri implementaciji Curry-Howardovega izomorfizma v OCamlu smo naleteli na tri ovire, ki pa smo jih v Leanu tudi že deloma rešili:
Z neomejeno rekurzijo lahko definiramo vrednosti poljubnega tipa, kar pomeni, da lahko dokažemo poljubno izjavo. Lean to reši tako, da nas omeji le na funkcije, za katere imamo dokaz o ustavitvi.
Pisanje dokazov neposredno s konstrukcijami vrednosti je nepraktično in ni v duhu dokazov, ki smo jih sicer navajeni. Lean to reši z uvedbo taktik, s katerimi ga usmerjamo pri konstrukciji dokazov.
S konstruktorji tipov lahko izrazimo le logične veznike, ne moremo pa izraziti kvantifikatorjev, kar pomeni, da smo v dokazih omejeni le na izjavni račun. Poglejmo si, kako v Leanu to rešimo z uvedbo odvisnih tipov.
Spomnimo se, da v Curry-Howardovem izomorfizmu vsaki izjavi ustreza tip, ki ima predstavnika natanko tedaj, kadar izjavo lahko dokažemo. Če želimo v izjavah uporabiti kvantifikatorje, moramo najprej govoriti o predikatih, torej izjavah, odvisnih od vrednosti. Naj bo \(S(n)\) izjava, da je naravno število \(n\) sodo. Tako je izjava \(S(42)\) resnična, izjava \(S(43)\) pa ne. Tip, ki bi ustrezal izjavi \(S(42)\) bi tako moral biti neprazen, tip \(S(43)\) pa prazen. Torej dobimo tip, ki se spreminja glede na vrednost števila \(n\). Do sedaj smo spoznali že:
Vrednosti, ki se spreminjajo glede na vrednosti: to so običajne funkcije kot je
podvoji : Nat → NatalizacetnaVrednost : (Float → Float) → Float.Tipe, ki se spreminjajo glede na tipe: to so konstruktorji tipov, na primer
List, ki iz tipa naravnih številNatustvari tip seznamov naravnih številList Nat. V OCamlu so bile take funkcije implicitne v parametričnih tipih, v Leanu pa imamo eksplicitno zapisano, da jeListfunkcija tipaType → Type. Imamo tudi funkcije z več argumenti, na primerProd : Type → Type → Type, ki iz tipovAinBnaredi njun produktA × B.Vrednosti, ki se spreminjajo glede na tipe: to morda ni bilo tako očitno, ampak v tej vlogi nastopajo polimorfne funkcije. Zadnjič smo videli, da moramo parametrično polimorfnim funkcijam v definicijah in klicih tipe podati kot eksplicitne argumente, na primer
polimorfni_stakni Int [1, 2, 3] [4, 5, 6]. Lahko bi sicer rekli, da polimorfne funkcije niso zares odvisne od tipov, saj se pri vsakem tipu obnašajo enako, ampak formalno gledano se funkcije vseeno spreminjajo, sajpolimorfni_stakni Intsprejme dva seznama celih števil,polimorfni_stakni Intpa ne. Odvisnost od tipov je še bolj očitna pri ad-hoc polimorfizmu. V Pythonu na primer imamo en sam+, ki deluje na različne načine: števila sešteva (pri čemer se6 * 7izračuna na popolnoma drug način kot6.0 * 7.0), nize in sezname stika, … V OCamlu imamo zaradi strogih tipov za vsako izmed teh različnih operacij svoj simbol:+za cela števila,+.za števila s plavajočo vejico,^za nize,@za sezname, … Lean podpira t.i. razrede tipov, ki nam omogočajo, da z enim samim simbolom varno in učinkovito predstavimo operacije pri različnih tipih. To je sicer tema za drugič, ampak je pa definitivno primer vrednosti, ki je dejansko odvisna od tipa.
Spoznajmo še tipe, ki se spreminjajo glede na vrednosti.
Vektorji#
Za začetek si tak tip definirajmo sami. Poznamo že tip seznamov, ki ga lahko v Leanu zapišemo kot:
inductive Seznam : Type where
| prazen : Seznam
| sestavljen : Int → Seznam → Seznam
ali kot
inductive Seznam : Type → Type where
| prazen {A : Type} : Seznam A
| sestavljen {A : Type} : A → Seznam A → Seznam A
če želimo parametrični polimorfizem.
Definirajmo še tip vektorjev, kar je v računalništvo ime za sezname z dano dolžino. Če je Seznam tip seznamov poljubne dolžine, potem bo Vektor 42 tip seznamov, ki vsebujejo natanko 42 celih števil, Vektor 0 pa tip, ki vsebuje le prazen seznam. V Leanu tip definiramo kot:
inductive Vektor : Nat → Type where
| prazen : Vektor Nat.zero
| sestavljen {n : Nat} : Int → Vektor n → Vektor (Nat.succ n)
Prazen vektor je torej tipa Vektor 0, sestavljen vektor pa vsebuje glavo (tipa Int) in rep (dolžine n), sam pa je dolžine n + 1. Tako kot smo stikali sezname z
def stakniSeznam : Seznam → Seznam → Seznam :=
fun xs ys =>
match xs with
| Seznam.prazen => ys
| Seznam.sestavljen x xs' =>
Seznam.sestavljen x (stakniSeznam xs' xs')
lahko stikamo tudi vektorje:
def stakniVektor {m n : Nat} : Vektor m → Vektor n → Vektor (n + m) :=
fun xs ys =>
match xs with
| Vektor.prazen => ys
| Vektor.sestavljen x xs' =>
Vektor.sestavljen x (stakniVektor xs' ys)
pri čemer bo Lean popazil, da se bodo dolžine dejansko ujemale. Če bi se na primer pri funkciji stakniSeznam zatipkali in zapisali Seznam.prazen => xs, se Lean ne bi pritožil (oz. bi se, da spremenljivke ys nismo uporabili, vendar bi definicijo vseeno sprejel). Če pa bi zapisali Vektor.prazen => xs, pa bi dobili napako
Type mismatch
xs
has type
Vektor m
but is expected to have type
Vektor (n + 0)
Opazimo lahko, da smo tip funkcije podali kot Vektor m → Vektor n → Vektor (n + m) in ne kot Vektor m → Vektor n → Vektor (m + n), kar bi bilo bolj smiselno. To je posledica tega, da je seštevanje naravnih števil v Leanu definirano rekurzivno po drugem argumentu, torej kot m + Nat.succ n = Nat.succ (m + n), stikanje vektorjev pa po prvem. Če torej vzamemo sestavljen vektor xs : Vektor (Nat.succ m) in poljuben vektor ys : Vektor n, za rep velja xs' : Vektor m. Torej bo rekurzivni klic stakniVektor xs' ys tipa Vektor (n + m). Če temu dodamo še glavo x, dobimo rezultat tipa Vektor (Nat.succ (n + m)), kar je po definiciji seštevanja enako kot Vektor (n + Nat.succ m), kar smo tudi pričakovali pri argumentih tipa Vektor (Nat.succ m) in Vektor n.
Seveda lahko definiramo tudi funkcijo Vektor m → Vektor n → Vektor (m + n), vendar se tu Lean pritoži že pri primeru stikanja s praznim seznamom. Tam za rezultat pričakuje Vektor (0 + n), mi pa smo mu dali ys : Vektor n. Po definicij namreč velja samo n + 0 = n, ne pa tudi 0 + n = n. To moramo dokazati ločeno! V tem primeru lahko vstopimo v taktični način, ker za cilj dobimo Vektor (0 + n). Nato uporabimo taktiko rw, ki ji kot argument podamo že dokazano trditev zero_add : ∀ (n : Nat), 0 + n = n, s pomočjo katere cilj zamenja v Vektor n, ki mu lahko zadostimo z ys. Podobno s trditvijo succ_add : ∀ (n m : Nat), (succ n) + m = succ (n + m) prepričamo Lean tudi v drugem primeru:
def stakniVektor' : Vektor m -> Vektor n -> Vektor (m + n) :=
fun xs ys =>
match xs with
| Vektor.prazen => by
rw [Nat.zero_add]
exact ys
| Vektor.sestavljen x xs' => by
rw [Nat.succ_add]
exact .sestavljen x (stakniVektor' xs' ys)
Predikatni račun#
S pomočjo odvisnih tipov lahko sedaj definiramo tudi zgoraj omenjeni tip, ki je pri sodih številih neprazen, pri lihih pa je prazen:
inductive NeprazenPriSodih : Nat → Type where
| nicJeSodo : NeprazenPriSodih Nat.zero
| sodoPlusDvaJeSodo {n : Nat} : NeprazenPriSodih n → NeprazenPriSodih (Nat.succ (Nat.succ n))
Gre za manjšo prilagoditev tipa Vektor. Tu imamo en element pri tipu NeprazenPriSodih 0, ter po en element pri tipu NeprazenPriSodih (n + 2), če imamo “rep” tipa NeprazenPriSodih n. Hitro lahko vidimo, da ima tip NeprazenPriSodih n predstavnika natanko takrat, kadar je n sod. Seveda je še bolje, če namesto Type uporabimo Prop:
inductive Sodo : Nat → Prop where
| nicJeSodo : Sodo Nat.zero
| sodoPlusDvaJeSodo {n : Nat} : Sodo n → Sodo (Nat.succ (Nat.succ n))
Za vajo lahko dokažete vsota_sodih {m n : Nat} : Sodo m → Sodo n → Sodo (m + n). Mi pa si za primer dokaza v predikatnem računu oglejmo paradoks brivca: brivec, ki bi bril vse, ki se ne brijejo sami, ne obstaja. Predpostavimo, da imamo tip ljudi C ter predikat B : C → C → Prop, ki pove, ali prva oseba brije drugo.
variable (C : Type)
variable (B : C → C → Prop)
theorem paradoks_brivca : ¬∃ (b : C), ∀ (c : C), B c c ↔ ¬B b c := by
-- cilj: ¬∃ b, ∀ (c : C), B c c ↔ ¬B b c
intro H
-- predpostavka H: ∃ b, ∀ (c : C), B c c ↔ ¬B b c
-- cilj: False
cases H -- razstavimo eksistenčni kvantifikator
rename_i bob koga_brije_bob -- eksplicitno poimenujemo brivca in njegovo lastnost
-- predpostavka koga_brije_bob: ∀ (c : C), B c c ↔ ¬B bob c
have kako_se_brije_bob := koga_brije_bob bob -- poglejmo, ali Bob brije samega sebe
-- predpostavka kako_se_brije_bob : B bob bob ↔ ¬B bob bob
-- pokažimo, da se B ne brije sam
have bob_se_ne_brije : ¬B bob bob := by
-- cilj vmesne leme: ¬B bob bob
intro bob_se_brije -- pokažimo negacijo
-- predpostavka bob_se_brije: B bob bob
-- cilj: False
have bob_se_ne_brije := kako_se_brije_bob.mp bob_se_brije -- uporabimo Bobovo lastnost
-- predpostavka bob_se_ne_brije: ¬B bob bob
contradiction -- protislovje
-- s have dobimo:
-- predpostavka bob_se_ne_brije : ¬B bob bob
-- cilj ostaja: False
have bob_se_brije := kako_se_brije_bob.mpr bob_se_ne_brije -- spet uporabimo Bobovo lastnost
contradiction -- protislovje