Ciele
- spustiť Rocq, pracovať v RocqIDE a rozlíšiť term Galliny, príkaz Vernacular a taktiku;
- pomocou
Definition,fun,Check,ComputeaPrintdefinovať a skúmať funkcie a ich typy; - definovať induktívny typ, spracovať ho pomocou
match, napísať jednoduchýFixpointa vysvetliť úplnosť vzorov a kontrolu terminácie. - sledovať dôkazový stav a zostaviť jednoduché dôkazy pomocou základných taktík.
Postup
Krok 1
Rocq ako nástroj teórie typov
Rocq v predmete Teória typov používame na štúdium vzťahu medzi typovaním, výpočtom a dokazovaním. V tomto cvičení definujeme dátové typy a funkcie, preskúmame typy termov a vyhodnotíme ich aplikácie. Na tieto základy nadviaže formulácia tvrdení a konštrukcia formálnych dôkazov.
Rocq je interaktívny dokazovací systém. Jeho jadro vychádza z Calculus of Inductive Constructions (CIC), teórie typov spájajúcej závislé funkčné typy, induktívne definície a konštruktívne dokazovanie. V Rocqu možno definovať matematické objekty, programy, tvrdenia aj dôkazy. Správnosť formálneho dôkazu napokon kontroluje malé dôveryhodné jadro.
Rocq obsahuje čistý funkcionálny jazyk Gallina, ktorý používame na zápis termov, funkcií, typov a tvrdení. Keď napríklad definujeme funkciu, systém overí, že jej telo má deklarovaný typ. Neskôr budeme rovnakú myšlienku typovej kontroly používať pri dôkazoch.
Poznámka
V softvérovom inžinierstve Rocq umožňuje overovať presne formulované vlastnosti programov. Príkladom je CompCert, prekladač jazyka C, pri ktorom boli v Rocqu formálne overené dôležité vlastnosti prekladu. Formálna verifikácia sa vždy vzťahuje na konkrétnu špecifikáciu a model; sama osebe nie je zárukou bezchybnosti ľubovoľného softvéru.
Vrstvy zápisu v Rocqu
Pri práci s Rocqom sa stretávame s viacerými vrstvami zápisu:
- Gallina je jazyk termov. Termom môže byť číslo, funkcia, aplikácia funkcie alebo výraz
match; zapisujeme ním aj matematické tvrdenia. - Vernacular obsahuje príkazy, ktoré menia globálne prostredie alebo sa ho dopytujú. Patria sem
Definition,Fixpoint,Inductive,Check,Compute,Print,LemmaaTheorem. Príkaz sa končí bodkou; term môže byť jeho súčasťou. - Taktické jazyky, najmä Ltac a Ltac2, slúžia na interaktívne vytváranie dôkazov. Ich kroky spracúvajú aktuálne dôkazové ciele.
Napríklad v príkaze Check 3. je Check príkaz Vernacular a 3 je term Galliny. Rovnako Definition double (n : nat) : nat := n + n. je príkaz, v ktorom je n + n telom funkcie v Galline.
Poznámka
Nasledujúcu ukážku dôkazu možno vykonať v Rocqu. Taktiky intros, induction, simpl a rewrite budeme podrobne vysvetľovať až v ďalšom cvičení. Teraz si stačí všimnúť hranicu medzi tvrdením a jeho dôkazom.
Lemma plus_0_r : forall n : nat, n + 0 = n.
Proof.
intros n.
induction n as [| n IHn].
- reflexivity.
- simpl. rewrite IHn. reflexivity.
Qed.
Inštalácia Rocqu
Postup inštalácie pre svoj operačný systém nájdete na oficiálnej stránke Rocqu. Nainštalujte Rocq a pracovné prostredie, v ktorom budete otvárať súbory s príponou .v.
Použiť možno viacero prostredí:
- RocqIDE poskytuje editor, zobrazenie cieľov a správ i interaktívne krokovanie.
- Visual Studio Code s rozšírením VsRocq poskytuje podporu Rocqu; podľa spôsobu inštalácie môže byť potrebný aj zodpovedajúci jazykový server.
- Emacs s rozšírením Proof General je ďalšia možnosť interaktívnej práce.
V rámci tohto cvičenia budeme primárne používať RocqIDE, aby sme mohli sledovať rovnaké okná a postupy. Príklady v súboroch .v možno vykonať aj v inom podporovanom prostredí.
Ovládanie RocqIDE
RocqIDE má editor zdrojového súboru, okno aktuálnych cieľov Goals, okno správ Messages a ovládací panel. V editore píšeme definície, výpočty aj dôkazy. V prvej lekcii bude väčšina príkazov definíciami a dopytmi; okno Goals bude dôležité najmä pri neskoršom dokazovaní.
- Editor je hlavné textové okno. Príkazy v ňom možno postupne odosielať systému. Na obrázku je editor vľavo.
- Goals zobrazuje aktuálny dôkazový cieľ a prípadné podciele. Ak práve nedefinujeme dôkaz, nemusí obsahovať nič, čo by bolo potrebné riešiť.
- Messages zobrazuje výsledky príkazov
Check,PrintaCompute, ako aj upozornenia a chybové hlásenia.
Ovládací panel umožňuje vykonať ďalší príkaz (Next), vrátiť sa o krok (Previous), vrátiť spracovanie na začiatok (Reset) alebo skontrolovať zvyšok súboru (Run to end). Krokovanie sa vzťahuje na príkazy v súbore, nielen na dôkazové taktiky. Klávesové skratky sa môžu líšiť podľa verzie RocqIDE a operačného systému; aktuálne priradenie si overte v ponuke aplikácie.
Prvý term, typ a výpočet
Do nového súboru .v vložte tieto príkazy a vykonávajte ich po jednom:
Check 3.
Compute 2 + 3.
Definition double (n : nat) : nat :=
n + n.
Check double.
Compute double 4.
Print double.
Výstupy príkazov v poradí, v akom sa vykonajú:
3 : nat
= 5
: nat
double : nat -> nat
= 8
: nat
Skrátený výstup Print double.:
double = fun n : nat => n + n
: nat -> nat
Ďalšie pomocné riadky výpisu Print sa môžu medzi verziami líšiť.
Číslo 3 je term typu nat, čo zistí Check 3.. Compute 2 + 3. vyhodnotí term a zobrazí výsledok 5. Príkaz Definition zavedie meno double pre funkciu, ktorá svoj vstup sčíta so sebou. Check double. vypíše funkčný typ nat -> nat; Compute double 4. vypíše 8. Print double. zobrazí uloženú definíciu a jej typ.
Výsledky príkazov sa zobrazujú v Messages. Check zisťuje typ, Compute vykonáva výpočet a Print ukazuje definíciu. Funkčný typ nat -> nat hovorí, že double prijíma prirodzené číslo a vracia prirodzené číslo.
Krok 2
Termy a funkcie v Galline
Funkcionálne programovanie stavia výpočty na tvorbe a aplikácii funkcií. Gallina je čistý funkcionálny jazyk používaný v Rocqu na zápis termov, funkcií, dátových typov a matematických objektov. Pri štúdiu teórie typov nám umožní sledovať, aký typ má každý term a ako sa jeho vyhodnotenie viaže na typové pravidlá.
- Nemennosť: Definované meno označuje term; jeho hodnotu nemeníme priradením ako pri meniteľnej premennej. Nový lokálny názov možno zaviesť pomocou
let. - Čisté funkcie: Výsledok funkcie závisí od vstupov a jej vyhodnotenie nemení vonkajší stav programu.
- Funkcie ako hodnoty: Funkcie v Galline môžu byť argumentmi iných funkcií aj ich výsledkami. Toto je jedna z vlastností funkcionálneho programovania, nie jeho jediná definícia.
Komentáre
Komentár je uzatvorený medzi (* a *). Môže zaberať jeden riadok alebo viac riadkov; samostatný symbol pre jednoriadkový komentár sa nepoužíva. Napríklad:
(* Nasledujúci príkaz vypočíta súčet. *)
Compute 1 + 1. (* Výsledok je 2. *)
Definície a základné príkazy Vernacular
Príkaz Definition zavedie globálne meno pre term. Nasledujúca definícia nepredstavuje priradenie do meniteľnej programovej premennej:
Definition x : nat := 10.
Check x.
Print x.
Compute x + 2.
Výstup Check x. a Compute x + 2.:
x : nat
= 12
: nat
Skrátený výstup Print x.:
x = 10
: nat
Meno x označuje term 10. Check x. zistí typ nat, Print x. vypíše definíciu a Compute x + 2. vypočíta 12. Ak je typ z termu jednoznačný, Rocq ho často dokáže odvodiť; explicitná anotácia : nat je pri výklade užitočná.
Rovnaký príkaz môže definovať funkciu:
Definition increment (n : nat) : nat := n + 1.
Check increment.
Print increment.
Compute increment 4.
Výstup Check increment. a Compute increment 4.:
increment : nat -> nat
= 5
: nat
Skrátený výstup Print increment.:
increment = fun n : nat => n + 1
: nat -> nat
Funkcia increment má typ nat -> nat. V zobrazení Print ju možno vidieť aj ako anonymnú funkciu fun n : nat => n + 1; presný formát ďalších riadkov výpisu sa môže medzi verziami Rocqu líšiť.
Ďalšie dopytovacie príkazy
Pri prvých príkladoch používajte najmä Check, Compute a Print. Na skúmanie existujúcich objektov sú užitočné aj tieto príkazy:
About Nat.add.
Search Nat.add.
Locate "+".
Print Scope nat_scope.
Skrátený výstup:
Nat.add : nat -> nat -> nat
Notation "x + y" := (Nat.add x y) : nat_scope
Search môže nájsť viac objektov a úplný výpis závisí od aktuálne načítaných knižníc. Uvedená notácia je podstatná časť výstupu Locate "+". a Print Scope nat_scope..
About poskytuje informácie o objekte, Search vyhľadáva podľa zadaného termu alebo tvaru a Locate zisťuje, ku ktorým definíciám sa môže viazať notácia +. Symbol + môže v rôznych kontextoch označovať rôzne operácie. Rozsah notácie (scope) pomáha určiť jeho interpretáciu; Print Scope nat_scope. ukáže notácie v rozsahu prirodzených čísel. Vlastné notácie zavedie samostatná časť nižšie.
Základné typy a hierarchia typov
Každý term má typ. Aj typy sú v Rocqu termami: napríklad true má typ bool a samotný bool má typ Set. Nasledujúca tabuľka odlišuje dátové typy od univerz, do ktorých tieto typy patria.
| Typ alebo univerzum | Kde patrí | Význam | Príklady |
|---|---|---|---|
Set |
Type |
univerzum malých dátových typov | nat, bool, list nat |
Prop |
Type |
univerzum logických tvrdení | True, False, n = 0 |
Typeᵢ |
Typeᵢ₊₁ |
univerzum na úrovni i |
typy nižších úrovní |
bool |
Set |
pravdivostné hodnoty | true, false |
nat |
Set |
prirodzené čísla | O, S O |
unit |
Set |
typ s jedinou hodnotou | tt |
Empty_set |
Set |
prázdny dátový typ | nemá konštruktor |
list nat |
Set |
zoznam prirodzených čísel | nil, cons 1 nil |
Zápis $Type_i : Type_{i+1}$ vyjadruje hierarchiu univerz: univerzum na úrovni i patrí do vyššieho univerza. Je to matematický opis, nie doslovná syntax príkazu Rocqu. Pri dopyte Check Type. sa môže zobraziť Type : Type, pretože Rocq v bežnom výstupe konkrétne úrovne skrýva. Neznamená to, že ide o ten istý typ na tej istej úrovni.
Check Set.
Check Prop.
Check Type.
Typický výstup:
Set : Type
Prop : Type
Type : Type
Pri základných dátových typoch a ich hodnotách:
Check bool.
Check true.
Check false.
Check nat.
Check O.
Check (S O).
Check unit.
Check tt.
Check Empty_set.
Výstup (Rocq môže zobrazovať O ako 0 a S O ako 1):
bool : Set
true : bool
false : bool
nat : Set
0 : nat
1 : nat
unit : Set
tt : unit
Empty_set : Set
Typ list očakáva parameter, ktorý určí typ prvkov zoznamu. Po dosadení nat dostaneme dátový typ list nat:
Check list.
Check (list nat).
Check (nil : list nat).
Check (cons 1 nil).
Výstup:
list : Type -> Type
list nat : Set
nil : list nat
(1 :: nil)%list : list nat
Zápis cons 1 nil môže Rocq vo výstupe zobraziť pomocou notácie ::. Napokon príklady tvrdení:
Check True.
Check False.
Check (3 = 4).
Check (forall n, n + 0 = n).
Výstup:
True : Prop
False : Prop
3 = 4 : Prop
forall n : nat, n + 0 = n : Prop
True a False sú tvrdenia, nie booleovské hodnoty true a false. Tvrdenie 3 = 4 je dobre typované ako objekt v Prop, hoci nie je pravdivé.
Anonymná funkcia a súvis s λ-kalkulom
Gallina zapisuje anonymnú funkciu v tvare fun x => t. Ide o obdobu λ-abstrakcie $\lambda x.t$ z prednášky o λ-kalkule. Identickú funkciu na prirodzených číslach môžeme definovať takto:
Definition id_nat : nat -> nat :=
fun n => n.
Check id_nat.
Compute id_nat 3.
Výstup:
id_nat : nat -> nat
= 3
: nat
Term fun n => n zodpovedá $\lambda n.n$. Anotácia nat -> nat určuje, že vstup aj výstup majú typ nat. Pri aplikácii id_nat 3 sa argument dosadí za parameter funkcie.
Funkcie vyššieho rádu a čiastočná aplikácia
Funkcia je hodnotou, a preto môže prijímať inú funkciu alebo ju vrátiť ako výsledok. Funkcie viacerých argumentov sa v Galline zapisujú ako postupnosť funkcií jedného argumentu. Preto aj aplikácia len na časť argumentov môže vrátiť funkciu.
Definition plus (a b : nat) : nat := a + b.
Definition plus2 := plus 2.
Check plus.
Check plus2.
Compute plus2 3.
Výstup:
plus : nat -> nat -> nat
plus2 : nat -> nat
= 5
: nat
Check plus. zobrazí nat -> nat -> nat. Šípka funkčného typu sa združuje sprava, takže ide o nat -> (nat -> nat): po prijatí prvého čísla zostáva funkcia z nat do nat. Výraz plus 2 má preto typ nat -> nat; Compute plus2 3. vráti 5. Tento spôsob reprezentácie viacargumentových funkcií sa nazýva currying.
Funkcia vyššieho rádu môže prijať funkciu ako argument:
Definition apply_twice (f : nat -> nat) (n : nat) : nat :=
f (f n).
Check apply_twice.
Compute apply_twice increment 3.
Výstup:
apply_twice : (nat -> nat) -> nat -> nat
= 5
: nat
Tu je prvým argumentom f funkcia nat -> nat; výsledok posledného výpočtu je 5.
Lokálne meno pomocou let ... in
Konštrukcia let zavedie lokálne meno iba v rámci daného termu. Nejde o meniteľnú premennú ani o novú globálnu definíciu:
Definition add_xy : nat :=
let a := 10 in
let b := 20 in
a + b.
Compute add_xy.
Výstup:
= 30
: nat
Výsledkom je 30. Mená a a b nie sú po skončení definície dostupné v globálnom prostredí.
Krok 3
Induktívne typy, analýza prípadov a rekurzia
Doteraz sme používali štandardné typy nat a bool. Teraz uvidíme, ako konštruktory vytvárajú hodnoty induktívneho typu, ako ich rozlišuje match a prečo pri rekurzii sledujeme štruktúru vstupu.
Induktívny typ a jeho konštruktory
Príkaz Inductive zavádza nový typ a jeho konštruktory. Jednoduchý nerekurzívny príklad predstavujú dni v týždni:
Inductive day : Type :=
| Monday
| Tuesday
| Wednesday
| Thursday
| Friday
| Saturday
| Sunday.
Check day.
Check Monday.
day je typ. Monday až Sunday sú jeho konštruktory; každý z nich vytvorí jednu hodnotu typu day. Konštruktory iných induktívnych typov môžu prijímať argumenty. V tomto príklade ich nemajú.
Všeobecný zápis má tvar Inductive MenoTypu : Type := ..., kde za := nasledujú konkrétne konštruktory a ich typy. Tri bodky tu vyjadrujú opis formátu, nie spustiteľný príkaz Rocqu. Typ konštruktorov závisí od definovaného typu; preto sa učíme prednostne z úplných príkladov.
Type označuje univerzum typov. Pri logických tvrdeniach sa neskôr stretneme aj s Prop. Podrobné rozlíšenie univerzov teraz nepotrebujeme.
Analýza podľa konštruktora: match
Výraz match vyberie vetvu podľa konštruktora spracúvanej hodnoty. V prípade typu day môžeme definovať nasledujúci kalendárny deň:
Definition next_day (d : day) : day :=
match d with
| Monday => Tuesday
| Tuesday => Wednesday
| Wednesday => Thursday
| Thursday => Friday
| Friday => Saturday
| Saturday => Sunday
| Sunday => Monday
end.
Compute next_day Friday.
Výstup:
= Saturday
: day
Výsledok next_day Friday je Saturday. Názov next_day zodpovedá tomu, že príklad počíta aj s víkendom.
Pri štandardnom type bool sú konštruktory true a false. Obe možnosti možno rozlíšiť priamo:
Print bool.
Definition negb_ok (b : bool) : bool :=
match b with
| true => false
| false => true
end.
Compute negb_ok true.
Výstup:
= false
: bool
Podobne pri type nat symbol 0 reprezentuje konštruktor O a vzor _ znamená „ľubovoľný iný prípad“:
Definition is_zero (n : nat) : bool :=
match n with
| 0 => true
| _ => false
end.
Compute is_zero 0.
Compute is_zero 3.
Výstupy:
= true
: bool
= false
: bool
Podčiarkovník _ nepomenuje zachytenú hodnotu, preto je vhodný vtedy, keď ju vetva nepotrebuje. V druhej vetve is_zero pokrýva všetky nenulové prirodzené čísla.
Úplnosť analýzy prípadov
Rocq vyžaduje, aby match pokryl všetky možné konštruktory spracúvaného typu. Nasledujúci príkaz ukazuje zámerne neúspešný pokus; predpona Fail potvrdí, že vnútornú definíciu Rocq odmietol, a umožní pokračovať vo vykonávaní súboru:
Fail Definition negb_bad (b : bool) : bool :=
match b with
| true => false
end.
Typ bool má aj konštruktor false, pre ktorý chýba vetva. Opravená definícia negb_ok vyššie pokrýva oba prípady. Toto je problém úplnosti vzorov; nesúvisí s termináciou rekurzie.
Bežné programovacie jazyky ako OCaml alebo Haskell môžu neúplný pattern matching akceptovať, často s upozornením. Ak počas behu nastane nepokrytý prípad, výpočet môže skončiť chybou. Rocq takúto definíciu funkcie neprijme.
Prirodzené čísla ako rekurzívny induktívny typ
Základnú myšlienku štandardnej definície nat možno znázorniť takto:
Inductive nat : Type :=
| O
| S (n : nat).
Tento zápis je zjednodušená ukážka, ktorú teraz nevykonávame: štandardný typ nat je už v prostredí zavedený. Konštruktor O predstavuje nulu, S n nasledovníka čísla n. Na rozdiel od day sa nat vyskytuje v type argumentu vlastného konštruktora S, preto je rekurzívny. Jeho skutočnú definíciu si môžete pozrieť príkazmi:
Print nat.
Check O.
Check S.
Compute S (S O).
Štruktúra O alebo S n nám dáva dve vetvy pre match a zároveň spôsob, ako pri rekurzii prejsť k menšiemu podtermu n.
Rekurzívne funkcie pomocou Fixpoint
Rekurzívna funkcia sa definuje príkazom Fixpoint. Pre faktoriál platí:
$$ \mathit{factorial}(n)= \begin{cases} 1, & n=0,\\ n\cdot\mathit{factorial}(n-1), & n>0. \end{cases} $$
Pri prirodzenom čísle tvaru S k platí n = S k. Rekurzívne volanie vykonáme na podterme k:
Fixpoint factorial (n : nat) : nat :=
match n with
| 0 => 1
| S k => n * factorial k
end.
Check factorial.
Compute factorial 5.
Výstup:
factorial : nat -> nat
= 120
: nat
Check factorial. vráti nat -> nat a Compute factorial 5. vráti 120. V druhej vetve je k štrukturálne menší podterm pôvodného argumentu S k; preto Rocq túto rekurziu akceptuje. Samotný fakt, že v tele vidíme menšie číslo, nestačí: kontrola sleduje vhodný štrukturálny vzťah medzi argumentmi.
Kontrola terminácie
Gallina povoľuje rekurzívnu definíciu iba vtedy, keď Rocq dokáže overiť jej termináciu. Bežnú neobmedzenú všeobecnú rekurziu preto priamo nepovoľuje. Nasledujúce volanie používa ten istý argument n, takže nespĺňa požiadavku štrukturálneho zmenšovania:
Fail Fixpoint loop (n : nat) : nat :=
loop n.
Fail opäť označuje zámerne odmietnutý pokus. Kontrastuje s factorial, kde volanie pracuje s podtermom k. Kontrola terminácie súvisí s logickými vlastnosťami systému a s výpočtami, ktoré Rocq používa pri kontrole typov. Zložitejšie spôsoby definovania terminujúcich funkcií budeme študovať neskôr.
Moduly a vlastný typ nat
Poznámka
Modul nám umožní definovať vlastnú verziu nat, plus a mult bez zámeny so štandardnými názvami.
Modul vytvorí priestor mien. Objekt definovaný v module možno zvonka pomenovať kvalifikovaným názvom, napríklad NatPlayground.plus. Vlastnú verziu prirodzených čísel preto uzavrieme do modulu:
Module NatPlayground.
Inductive nat : Type :=
| O
| S (n : nat).
Fixpoint plus (n m : nat) : nat :=
match n with
| O => m
| S k => S (plus k m)
end.
Fixpoint mult (n m : nat) : nat :=
match n with
| O => O
| S k => plus m (mult k m)
end.
Compute plus (S O) (S O).
Compute mult (S (S O)) (S (S O)).
End NatPlayground.
Výstupy oboch výpočtov v module zobrazujú hodnoty zostavené z jeho vlastných konštruktorov:
= S (S O)
: nat
= S (S (S (S O)))
: nat
Vnútri modulu sa meno nat vzťahuje na NatPlayground.nat; pri výpise po uzavretí modulu Rocq môže použiť kvalifikované názvy.
V definícii plus sa prvý argument postupne mení z S k na k. V definícii mult sa ten istý menší podterm používa pri rekurzívnom volaní mult k m. V oboch prípadoch ide o štrukturálnu rekurziu. Po uzavretí modulu sú vlastné objekty dostupné napríklad takto:
Check NatPlayground.nat.
Check NatPlayground.plus.
Compute NatPlayground.plus
(NatPlayground.S NatPlayground.O)
(NatPlayground.S NatPlayground.O).
Výstup posledného výpočtu:
= NatPlayground.S (NatPlayground.S NatPlayground.O)
: NatPlayground.nat
Typy NatPlayground.nat a štandardný nat sú rôzne. Modul bráni kolízii mien pri písaní príkladov; pri odkazoch na objekty používame ich kvalifikované názvy. Nie je presné povedať, že vlastná definícia automaticky zmení definíciu štandardnej knižnice.
Notácie a rozsahy notácií
Notácia nemení význam funkcie; poskytuje alternatívny syntaktický zápis termu. Napríklad 3 + 4 je zápis aplikácie funkcie sčítania na dva argumenty:
Compute Nat.add 3 4.
Compute 3 + 4.
Locate "+".
Rocq môže viazať ten istý symbol na rôzne funkcie podľa rozsahu notácie (scope). Notácia pre prirodzené čísla je v nat_scope; medzi ďalšie používané rozsahy patria bool_scope, list_scope a type_scope. Niektoré rozsahy sa otvárajú podľa očakávaného typu, iné môžeme vybrať explicitne. Príkaz Locate ukazuje možné interpretácie symbolu a Print Scope nat_scope. vypíše notácie daného rozsahu.
Vlastnú notáciu môžeme bezpečne zaviesť v novom rozsahu. Nasledujúci samostatný modul pokračuje v práci s vlastným typom nat:
Module NatNotation.
Import NatPlayground.
Declare Scope playground_nat_scope.
Delimit Scope playground_nat_scope with pnat.
Notation "a + b" := (plus a b)
(at level 50, left associativity) : playground_nat_scope.
Notation "a * b" := (mult a b)
(at level 40, left associativity) : playground_nat_scope.
Compute ((S O) + (S O))%pnat.
Print Scope playground_nat_scope.
End NatNotation.
Parameter at level určuje prioritu: v bežnom rozsahu úrovní má nižšie číslo vyššiu prioritu, takže násobenie na úrovni 40 sa zoskupí skôr než sčítanie na úrovni 50. left associativity určuje zoskupovanie operátorov rovnakej priority zľava. Sufix %pnat vyberie vlastný rozsah; tým sa vyhneme zámene so štandardným + a *.
Polymorfizmus a implicitné argumenty
id_nat funguje iba na hodnotách typu nat. Polymorfná identická funkcia prijíma typ A a hodnotu tohto typu; výsledok má opäť typ A:
Definition id_poly (A : Type) (value : A) : A := value.
Definition id_poly2 : forall A : Type, A -> A :=
fun A value => value.
Check id_poly.
Compute id_poly nat 3.
Compute id_poly _ 3.
Výstup Check id_poly. a oboch výpočtov:
id_poly : forall A : Type, A -> A
= 3
: nat
= 3
: nat
Argument A určuje typ ďalšieho argumentu. V poslednom výpočte podčiarkovník žiada Rocq, aby A odvodil z typu hodnoty 3. Zápis forall A : Type, A -> A vyjadruje, že funkcia funguje pre každý typ A.
Ak parameter uzavrieme do zložených zátvoriek, stane sa implicitným argumentom. Bežne ho potom pri aplikácii vynechávame:
Definition id_poly3 {A : Type} (value : A) : A := value.
Compute id_poly3 3.
Compute @id_poly3 nat 3.
Compute id_poly3 (A := nat) 3.
Všetky tri výpočty vrátia:
= 3
: nat
Symbol @ vypne v danom použití implicitnosť, takže typ nat zapíšeme explicitne. Pomenovaný argument (A := nat) je druhý spôsob, ako typ uviesť bez zmeny ostatných argumentov. Tieto možnosti sa budú hodiť pri typovo všeobecných definíciách v ďalších lekciách.
Vzájomná rekurzia
Dve funkcie sa môžu volať navzájom. Príkaz with spája ich definície do jednej vzájomne rekurzívnej skupiny. Nasledujúce funkcie zisťujú paritu čísla:
Fixpoint is_even (n : nat) : bool :=
match n with
| 0 => true
| S k => is_odd k
end
with is_odd (n : nat) : bool :=
match n with
| 0 => false
| S k => is_even k
end.
Compute is_even 4.
Compute is_odd 5.
Výstupy oboch volaní:
= true
: bool
= true
: bool
Každé rekurzívne volanie pracuje s podtermom k čísla S k. Rocq teda môže overiť termináciu celej skupiny.
Extrakcia kódu
Z výpočtového obsahu definovaného v Galline možno generovať kód pre bežné funkcionálne jazyky, napríklad OCaml. Pri faktoriáli stačí načítať podporu extrakcie a určiť výstupný súbor:
From Corelib Require Extraction.
Extraction Language OCaml.
Extraction "factorial.ml" factorial.
Posledný príkaz vytvorí súbor factorial.ml v pracovnom adresári procesu Rocq. Extrakcia je samostatný ďalší krok po definovaní a typovej kontrole funkcie; výsledný súbor je vhodné osobitne skompilovať a otestovať. Rocq podporuje aj extrakciu do ďalších funkcionálnych jazykov.
Programovanie v Rocqu
Poznámka
Pracujte v jednom súbore .v a príkazy vykonávajte postupne. Vlastné typy definujte v moduloch, aby sa nemiešali so štandardnými typmi Rocqu.
Poznámka
Užitočné sú oficiálna dokumentácia Rocqu, starší prehľad syntaxe a prehľad taktík. Staršie pomôcky používajú názov Coq a môžu opisovať odlišnú verziu syntaxe.
Úloha 3.1
Definujte modul cvicenieBool na prácu s vlastným typom bool.
Úloha 3.2
V module definujte vlastný induktívny typ bool s konštruktormi pre pravdu a nepravdu. Pomocou Check zistite typ oboch konštruktorov.
Úloha 3.3
Pomocou match definujte funkcie nad vlastným typom bool s týmito názvami:
negb– negáciu,andb– konjunkciu,orb– disjunkciu,implb– implikáciu.
Úloha 3.4
Pomocou príkazov Check, Print a About vypíšte informácie o svojich definíciách. Porovnajte, čo zobrazí každý z príkazov.
Úloha 3.5
Príkazom Compute otestujte obe hodnoty negácie a vybrané dvojice vstupov ostatných funkcií. Aspoň jednu binárnu funkciu otestujte na všetkých štyroch dvojiciach hodnôt.
Úloha 3.6
Zaveďte vo vlastnom rozsahu notácie ! pre negáciu, &&& pre konjunkciu, ||| pre disjunkciu a ---> pre implikáciu. V Rocqu 9.2 tieto symboly fungujú pri deklarácii notácií vo vlastnom rozsahu. Nastavte priority v tomto poradí od najvyššej po najnižšiu: negácia, konjunkcia, disjunkcia, implikácia. Implikáciu nastavte ako pravostrannú, teda a ---> b ---> c sa má čítať a ---> (b ---> c). Pomocou Locate overte interpretáciu symbolov.
Úloha 3.7
Pomocou Compute otestujte výrazy s novými operátormi. Výsledky porovnajte s výpočtom, v ktorom použijete priamo názvy funkcií.
Úloha 3.8
Overte prioritu a asociativitu nasledujúcimi výpočtami po otvorení vlastného rozsahu notácií:
Compute (! false &&& false). (* false *)
Compute (true ||| true &&& false). (* true *)
Compute (false &&& true ---> false). (* true *)
Compute (false ---> true ---> false). (* true *)
Prvý výraz má význam (! false) &&& false, druhý true ||| (true &&& false) a tretí (false &&& true) ---> false. Posledný výraz musí mať význam false ---> (true ---> false); pri ľavostrannom zoskupení by bol výsledok false. Každý výsledok porovnajte aj s úplne uzátvorkovaným volaním funkcií.
Úloha 3.9
Ukončite modul cvicenieBool a overte, že k jeho objektom viete pristupovať kvalifikovanými názvami.
Úloha 3.10
Preskúmajte štandardný typ nat a funkcie nad ním. Použite Print nat., Check O., Check S. a About Nat.add.. Porovnajte štandardné definície s vlastným typom nat z modulu NatPlayground.
Úloha 3.11
V module NatPlayground definujte funkcie successor, pred a iszero. Vlastné hodnoty 0, 1 a 3 zostavte z konštruktorov O a S a použite ich pri testoch. Predchodca nuly má byť nula, teda pred O = O.
Úloha 3.12
V module NatPlayground implementujte pomocou Fixpoint funkcie plus_rec a times_rec podľa rovníc:
plus_rec O n = n,plus_rec (S m) n = S (plus_rec m n),times_rec O n = O,times_rec (S m) n = plus_rec n (times_rec m n).
Rekurzia v oboch funkciách prebieha podľa prvého argumentu. Pri každom rekurzívnom volaní určte, ako sa tento argument štrukturálne zmenšuje.
Poznámka
Compute iba vypíše výsledok výpočtu. Neoveruje automaticky, či je výsledok očakávaný; porovnajte ho s vlastným výpočtom.
Poznámka
Pri vlastnom type nat nemožno bez konverzie používať číselné literály ani štandardné operácie pre štandardný nat. Testovacie hodnoty zostavujte z konštruktorov O a S príslušného modulu.
Úloha 3.13
Vyskúšajte extrakciu vlastnej terminujúcej funkcie do OCaml a prezrite si vygenerovaný súbor.
Krok 4
II. časť: Prvé dôkazy
V prvej časti sme skúmali termy a ich typy. Teraz budeme dokazovať jednoduché tvrdenia a sledovať, ako taktiky menia dôkazový stav. Zobrazené stavy zodpovedajú Rocqu 9.2; pre čitateľnosť vynechávame iba rámec okna a hlásenie o počte cieľov. Odlišná verzia môže niektoré termy zobraziť inou notáciou.
Dôkazový stav
Cieľ (goal) je tvrdenie, ktoré treba dokázať. Nad vodorovnou čiarou je lokálny kontext: premenné s typmi a hypotézy, ktoré možno použiť. Pod čiarou je aktuálny cieľ. Napríklad:
n : nat
============================
n = n
Taktika transformuje dôkazový stav. Môže zaviesť premennú alebo hypotézu do kontextu, zjednodušiť cieľ, rozdeliť ho na podciele alebo ho uzavrieť. Proof. začína taktický dôkaz; Qed. ho uzavrie až vtedy, keď nezostáva žiadny podcieľ.
Zavedenie premennej: intro
Theorem refl_nat : forall n : nat, n = n.
Proof.
(* ============================
forall n : nat, n = n *)
intro n.
(* n : nat
============================
n = n *)
reflexivity.
Qed.
Na začiatku je kontext prázdny. Taktika intro n. presunie univerzálne kvantifikovanú premennú do lokálneho kontextu; oba stavy sú uvedené priamo v komentároch dôkazu.
Taktika reflexivity. dokáže rovnosť termu so sebou samým. Po jej vykonaní nezostáva žiadny podcieľ (No more goals), preto možno použiť Qed.. Meno možno aj vynechať; Rocq ho vtedy zvolí sám:
Theorem refl_nat' : forall n : nat, n = n.
Proof.
intro.
reflexivity.
Qed.
Zavedenie viacerých objektov a použitie hypotézy
Theorem intros_example :
forall (n m : nat),
n + m = m + n ->
n + m = m + n.
Proof.
intros n m H.
(* n, m : nat
H : n + m = m + n
============================
n + m = m + n *)
exact H.
Qed.
Príkaz intros n m H. tu vykoná tri zavedenia, ekvivalentne intro n. intro m. intro H.. Výsledný stav je v komentári. Hypotéza H má presne typ aktuálneho cieľa. Taktika exact H. preto cieľ uzavrie; nezostáva žiadny podcieľ.
Výpočet v cieli: simpl a reflexivity
Theorem dva_plus_tri : 2 + 3 = 5.
Proof.
(* ============================
2 + 3 = 5 *)
simpl.
(* ============================
5 = 5 *)
reflexivity.
Qed.
Komentáre zachytávajú cieľ pred simpl a po ňom. simpl vykoná redukciu výrazu 2 + 3. reflexivity potom uzavrie rovnosť. Pri tomto konkrétnom cieli možno simpl vynechať, pretože reflexivity využíva aj konverziu termov:
Theorem dva_plus_tri' : 2 + 3 = 5.
Proof.
reflexivity.
Qed.
Sčítanie prirodzených čísel je definované rekurziou podľa prvého argumentu. Preto sa výraz 0 + n zjednoduší aj pri neznámom n:
Theorem zero_plus_n : forall n : nat, 0 + n = n.
Proof.
intro n.
(* n : nat
============================
0 + n = n *)
simpl.
(* n : nat
============================
n = n *)
reflexivity.
Qed.
Komentáre ukazujú, že po intro n. je cieľ 0 + n = n a po simpl. sa zmení na n = n.
Pri n + 0 je prvý argument neznámy: Rocq nemôže iba výpočtom rozhodnúť, či ide o O alebo S k. Samotný simpl preto všeobecne nepremení cieľ n + 0 = n na reflexívnu rovnosť. Dôkaz takéhoto tvrdenia bude neskôr motivovať indukciu.
Prepísanie cieľa: rewrite
Rovnosť z kontextu možno použiť na nahradenie výrazu v cieli:
Theorem example_rewrite :
forall n m : nat,
n = m ->
n + 1 = m + 1.
Proof.
intros n m H.
(* n, m : nat
H : n = m
============================
n + 1 = m + 1 *)
rewrite H.
(* n, m : nat
H : n = m
============================
m + 1 = m + 1 *)
reflexivity.
Qed.
Komentáre ukazujú, ako rewrite H. prepíše výskyt n v cieli na m; hypotéza zostáva v kontexte.
rewrite H používa rovnosť zľava doprava. Opačný smer zapisujeme rewrite <- H:
Theorem example_rewrite_backwards :
forall n m : nat,
n = m ->
m + 1 = n + 1.
Proof.
intros n m H.
rewrite <- H.
(* n, m : nat
H : n = m
============================
n + 1 = n + 1 *)
reflexivity.
Qed.
Po spätnom prepísaní má cieľ tvar uvedený v komentári dôkazu.
Prepísať možno aj pomocou už dokázanej lemy:
Theorem plus_O_n : forall n : nat, 0 + n = n.
Proof.
intro n.
simpl.
reflexivity.
Qed.
Theorem example_rewrite_with_theorem :
forall n : nat, 0 + n + 1 = n + 1.
Proof.
intro n.
(* n : nat
============================
0 + n + 1 = n + 1 *)
rewrite plus_O_n.
(* n : nat
============================
n + 1 = n + 1 *)
reflexivity.
Qed.
Rocq v tomto prípade odvodí argument lemy z výskytu 0 + n v cieli; stav pred prepísaním aj po ňom je v komentári. Smer prepísania má zásadný vplyv na podobu cieľa:
Theorem example_rewrite_chain :
forall n,
n = n + 0 ->
n + 1 = (n + 0) + 1.
Proof.
intros n H.
(* n : nat
H : n = n + 0
============================
n + 1 = n + 0 + 1 *)
rewrite <- H.
(* n : nat
H : n = n + 0
============================
n + 1 = n + 1 *)
reflexivity.
Qed.
rewrite <- H. nahradí výskyt n + 0 na pravej strane za n, takže zostane n + 1 = n + 1. Oba stavy sú v komentároch. Pozrime sa aj na zámerne nedokončený variant:
Theorem example_rewrite_chain' :
forall n,
n = n + 0 ->
n + 1 = (n + 0) + 1.
Proof.
intros n H.
rewrite H.
(* n : nat
H : n = n + 0
============================
n + 0 + 1 = n + 0 + 0 + 1 *)
Admitted.
rewrite H. je syntakticky platný, ale ide nevhodným smerom: nahrádza n zložitejším n + 0. Výsledný stav je v komentári. Takto zvolený krok nevyriešil cieľ. Pre tento dôkaz je vhodnejší smer rewrite <- H..
Admitted: nedokončený dôkaz
Admitted. ukončí otvorený dôkaz bez vyriešenia zostávajúcich podcieľov. Rocq potom tvrdenie prijíma ako predpokladaný výsledok, ale nejde o overený dôkaz tohto tvrdenia: v dôkaze vzniká diera. Počas prípravy súboru to môže byť užitočné, vo finálnom riešení však treba Admitted. nahradiť dôkazom ukončeným Qed.. Úspešné spracovanie súboru .v s Admitted. neznamená, že všetky jeho tvrdenia majú dokončený formálny dôkaz.
Analýza prípadov: destruct
Nasledujúce tvrdenie hovorí, že dvojnásobná negácia booleovskej hodnoty vráti pôvodnú hodnotu:
Theorem example_destruct_bool :
forall b : bool,
negb (negb b) = b.
Proof.
intros b.
(* b : bool
============================
negb (negb b) = b *)
destruct b.
(* 2 ciele:
1. ============================
negb (negb true) = true
2. ============================
negb (negb false) = false *)
- reflexivity.
- reflexivity.
Qed.
Komentáre ukazujú cieľ s b : bool aj dva podciele vytvorené taktikou destruct b.. V každej vetve sa po dosadení konkrétneho konštruktora obe negácie vypočítajú. reflexivity uzavrie najprv prvý, potom druhý cieľ. Zápis - pomáha oddeliť vetvy dôkazu.
Pri prirodzených číslach vzniknú prípady O a S n'. Symbol \/ je logická disjunkcia („alebo“):
Theorem example_destruct_nat :
forall n : nat,
n = 0 \/ n = S (Nat.pred n).
Proof.
intros n.
(* n : nat
============================
n = 0 \/ n = S (Nat.pred n) *)
destruct n as [| n'].
(* 2 ciele:
1. ============================
0 = 0 \/ 0 = S (Nat.pred 0)
2. n' : nat
============================
S n' = 0 \/ S n' = S (Nat.pred (S n')) *)
- left.
(* ============================
0 = 0 *)
reflexivity.
- right.
(* n' : nat
============================
S n' = S (Nat.pred (S n')) *)
reflexivity.
Qed.
Komentáre zachytávajú oba prípady po destruct n as [| n'] aj účinok taktík left a right. Nat.pred (S n') sa podľa definície Nat.pred vypočíta na n', preto aj druhý cieľ uzavrie reflexivity. V syntaxi as [| n'] prvé, prázdne miesto patrí konštruktoru O bez argumentu. Meno n' za zvislou čiarou označuje argument konštruktora S.
Rozdelenie cieľa a vnorené vetvy
Taktika split rozdelí konjunkciu A /\ B na ciele A a B. Pri viacerých úrovniach vetvenia používame značky vetiev (bullets):
Theorem example_bullets :
forall b1 b2 : bool,
b1 = b1 /\ b2 = b2.
Proof.
intros b1 b2.
(* b1, b2 : bool
============================
b1 = b1 /\ b2 = b2 *)
split.
(* 2 ciele:
1. b1, b2 : bool
============================
b1 = b1
2. b1, b2 : bool
============================
b2 = b2 *)
- destruct b1.
(* 2 ciele v prvej vetve:
1. b2 : bool
============================
true = true
2. b2 : bool
============================
false = false *)
+ reflexivity.
+ reflexivity.
- destruct b2.
+ reflexivity.
+ reflexivity.
Qed.
Komentáre ukazujú stav po intros, podciele po split aj rozdelenie prvej vetvy po destruct b1. Značka - zaostrí prvú vetvu; druhý cieľ zostane zatiaľ nezaostrený. Pre vnorenú úroveň použijeme +. Po uzavretí oboch prípadov sa vrátime k druhému cieľu pôvodného split; ten riešime obdobne cez destruct b2.. Pri ďalšej úrovni vetvenia možno použiť *. Značky organizujú dôkaz, samy však nič nedokazujú.
Prehľad použitých taktík
| Taktika | Základný účinok |
|---|---|
intro x |
presunie premennú alebo predpoklad do kontextu |
intros |
vykoná viac zavedení naraz |
exact H |
uzavrie cieľ termom presného typu, napríklad hypotézou H |
simpl |
zjednoduší výrazy výpočtom |
reflexivity |
dokáže rovnosť konvertibilných termov |
rewrite H |
prepíše pomocou rovnosti zľava doprava |
rewrite <- H |
prepíše sprava doľava |
destruct x |
rozdelí dôkaz podľa konštruktorov hodnoty x |
split |
rozdelí vhodný zložený cieľ, napríklad konjunkciu |
left |
zvolí ľavú vetvu disjunkcie |
right |
zvolí pravú vetvu disjunkcie |
Tabuľka zachytáva účinok taktík použitých v tejto lekcii; nie je úplným prehľadom taktického jazyka.
Samostatné úlohy – Prvé dôkazy
Poznámka
V každej úlohe doplňte dôkaz medzi Proof. a Qed.. Zobrazené Admitted. je iba dočasná kostra: odstráňte ho a nahraďte úplným dôkazom. Používajte predovšetkým taktiky intro, intros, exact, simpl, reflexivity, rewrite a destruct vysvetlené vyššie.
Úloha 2.1 – Reflexivita pre bool
Dokážte, že každá hodnota typu bool je rovná sama sebe.
Theorem bool_self :
forall b : bool, b = b.
Proof.
(* TODO *)
Admitted.
Úloha 2.2 – Konkrétny výpočet
Dokážte, že 4 + 0 = 4.
Theorem four_plus_zero :
4 + 0 = 4.
Proof.
(* TODO *)
Admitted.
Úloha 2.3 – Použitie hypotézy
Zaveďte premenné a predpoklad do kontextu a použite daný predpoklad na uzavretie cieľa.
Theorem use_assumption :
forall (n m : nat),
n = m -> n = m.
Proof.
(* TODO *)
Admitted.
Úloha 2.4 – Prepísanie rovnosťou
Dokážte tvrdenie pomocou predpokladu n = m.
Theorem plus_two :
forall (n m : nat),
n = m ->
n + 2 = m + 2.
Proof.
(* TODO *)
Admitted.
Úloha 2.5 – Prepísanie dokázanou lemou
Najprv dokážte add_zero_left. V dôkaze plus_zero_example potom explicitne použite rewrite add_zero_left.; samotný automatický výpočet nestačí.
Theorem add_zero_left :
forall n : nat,
0 + n = n.
Proof.
(* TODO *)
Admitted.
Theorem plus_zero_example :
forall n : nat,
0 + n + 3 = n + 3.
Proof.
(* TODO: použite rewrite add_zero_left. *)
Admitted.
Úloha 2.6 – Analýza prípadov pre bool
Nasledujúce príkazy ukazujú správanie štandardných booleovských funkcií:
From Stdlib Require Import Bool.Bool.
Compute (true && true).
Compute (true || true).
Compute (negb true).
Výstupy sú postupne true, true a false, vždy typu bool. Tvrdenie dokážte analýzou prípadov hodnoty b:
Theorem orb_true :
forall b : bool,
b || true = true.
Proof.
(* TODO *)
Admitted.
Úloha 2.7 – Výpočet bez destruct
Dokážte tvrdenie výpočtom podľa definície sčítania. destruct nie je potrebný, pretože sčítanie nat je definované rekurziou podľa prvého argumentu.
Theorem one_plus_n :
forall n : nat,
1 + n = S n.
Proof.
(* TODO *)
Admitted.
Úloha 2.8 – Tranzitivita rovnosti
Dokážte tvrdenie použitím zavedených hypotéz a prepísania.
Theorem trans_eq :
forall n m k : nat,
n = m ->
m = k ->
n = k.
Proof.
(* TODO *)
Admitted.
Zhrnutie
Gallina umožňuje zapisovať termy, funkcie a induktívne typy; príkazy Vernacular, najmä Check, Compute a Print, skúmajú ich typy, výpočty a definície. Funkčné typy, anonymné funkcie, match a štrukturálna rekurzia ukazujú, ako spolu súvisia typovanie, výpočet a kontrola terminácie. Set, Prop a hierarchia Type odlišujú dátové typy, tvrdenia a univerzá.
Pri dokazovaní sledujeme lokálny kontext, aktuálny cieľ a podciele. intro a intros zavádzajú objekty do kontextu, exact používa term presného typu, simpl vykonáva redukciu, reflexivity uzatvára konvertibilnú rovnosť, rewrite prepíše výraz podľa rovnosti a destruct rozdelí dôkaz podľa konštruktorov. split, left a right pracujú so zloženými tvrdeniami; značky -, + a * organizujú vetvy dôkazu. V ďalšej prednáške na tieto základy nadviažeme pri zložitejších dôkazoch a indukcii.
Zdroje
- The Rocq Prover: oficiálna dokumentácia.
- Paulin-Mohring, Christine. „Introduction to the Calculus of Inductive Constructions.“ All about Proofs, Proofs for All 55 (2015).
- Bertot, Yves, a Pierre Castéran. Interactive Theorem Proving and Program Development: Coq’Art: The Calculus of Inductive Constructions. Springer, 2013.
- Bertot, Yves. „Coq in a Hurry.“ arXiv:cs/0603118 (2006).
- Huet, Gérard, Gilles Kahn a Christine Paulin-Mohring. The Coq Proof Assistant: A Tutorial. Rapport Technique 178 (1997).
- В. Н. Крупский, С. Л. Кузнецов. Практикум по математической логике в COQ. Московский государственный университет имени М. В. Ломоносова, 2013.