Prendiamo come esempio la definizione di una
semplice funzione aritmetica f:
$$
f = x \mapsto x + 1
$$
$$
f : \mathcal{N} \to \mathcal{N}
$$
e traduciamola in Coq.
Nota: anche se nat, +, * e 1 sono concetti definibili
in Coq, l'ambiente iniziale non è vuoto ma contiene
alcuni declarazioni utili a fare esempi come i numeri
naturali e le operazioni di addizione e moltiplicazione
Concetti:
funzione anonima fun
definizione con nome Definition
sintassi per l'applicazione
comando Check per chiedere il tipo di un termine
Valutazione di funzioni (calcolo, esecuzione)
In Coq funzione = programma, ovvero le funzioni
definite vengono calcolate (eseguite) quando il
loro input è noto.
Chiediamo a Coq di calcolare la forma canonica
dei termini seguenti:
Esempio di XXX\betaXXX-riduzione (sostituzione di x
con l'argomento)
In questi due esempi f e g (nomi) sono sostituiti con i
propri valori
Concetti:
esecuzione di programmi Eval compute in
Tipi di dato algebrici
Facciamo finta che i numeri naturali
che abbiamo usato finora non siano disponibili
e definiamoli noi. Lo facciamo dentro un Module,
una specie di contenitore per evitare che Coq faccia
confusione tra
i concetti già esistenti e quelli che stiamo definendo.
true e false si chiamano costruttori, e
corrispondono alle regole XXXTipo-I_nXXX
nelle note del corso
I numeri naturali à la Peano
Nota: O è una o maiuscola, non la cifra zero
Nota: esistono varie sintassi alternative per
la dichiarazione di tipi di dato.
Quella che abbiamo utilizzato è la più simile a quella
usata nelle note del corso (e anche nei linguaggi di
programmazione). Se chiediamo a Coq di stampare una
dichiarazione di tipo con:
otteniamo la stampa in una sintassi alternativa.
Ora torniamo a utilizzare la dichiarazione standard di
nat in quanto il sistema li stampa/parsa meglio
(notazione decimale).
Concetti:
dichiarazione tipo induttivo Inductive
comando per stampare una dichiarazione induttiva Print
notazioni decimali per nat, e.g. 1 = S O
Pattern matching
In Coq l'eliminatore (chiamato XXXElXXX nelle note
del corso) è fornito in kit:
il costrutto match fornisce l'analisi per casi
il costrutto Fixpoint (in seguito) fornisce
l'ipotesi induttiva
Inoltre le regole computazionali dell'eliminatore
sono date dalla combinazione
di match e Fixpoint
Definiamo ora il predecessore
Esempio di esecuzione:
match S (S O) with
| O => O
| S p => p
end
O ==?== S (S O) no, proviamo la prossima clausola
S p ==?== S (S O) sì, p lega (S O)
Note:
Ogni clausola del pattern matching è della forma
| pattern => corpo
dove pattern è un costruttore applicato a un numero
di variabili appropriato
Tutti i costruttori del tipo di dato devono essere
trattati da un pattern (eg anche il caso O per pred)
p è una variabile legata dal pattern matching
e può essere utilizzata nel corpo della clausola
il termine a cui il match è applicato viene
messo in forma canonica. In altre parole i casi del
match parlano della forma canonica.
Concetti:
pattern matching match .. with .. end
forma delle clausole K x => v dove K è un costruttore
calcolo di una funzione definita via pattern matching
Ricorsione
Definiamo l'addizione sui numeri naturali.
$$
0 + m = m
$$
$$
S~p + m = S (p + m)
$$
Esempio di esecuzione:
addn 2
addn (S (S O)) 4
S (addn (S O) 4)
S (S (addn O 4))
S (S 4)
6
Non tutti i programmi ricorsivi sono accettati!
Concetti:
programmi ricorsivi Fixpoint
terminazione
Tipi di dato polimorfi
I tipi di dato polimorfi sono, in genere, dei contenitori
per aggregare vari termini. Inoltre vogliamo spesso
riutilizzare tali contenitori per aggregare termini
di tipo di volta in volta diverso.
Esempi classici:
La coppia XXX(a,b)XXX dove XXXa : AXXX e XXXb : BXXX
La lista XXXx_1 :: \ldots :: x_nXXX dove XXXx_i : AXXX
forall A B : Type, ci indica che pair è polimorfo,
ovvero A e B possono essere rimpiazzati per tipi di dato
a piacere
Se il tipo è quantificato, allora il valore
dell'argomento sostituisce la variabile in ciò
che rimane
pair : forall A, forall B, A -> B -> prod A B
pair nat : forall B, nat -> B -> prod nat B
pair nat bool : nat -> bool -> prod nat bool
pair nat bool 3 : bool -> prod nat bool
pair nat bool 3 false : prod nat bool
Concetti:
polimorfismo e quantificazione forall A : Type
tipaggio dell'applicazione in presenza di forall
pattern matching e parametri di tipo
Argomenti impliciti (inferiti da Coq)
I termini costruiti in precedenza, come
pair nat bool 3 false
sono molto verbosi e contengono informazione ridondante.
Concetti:
argomenti impliciti (inferenza)
Liste (omogenee) e notazione
Concetti:
liste
notazioni infisse
parametri tra {} sono impliciti
Ordine superiore e calcolo simbolico
Le funzioni possono essere passate ad altre funzioni
come argumenti
iter f 3 x = f (f (f x))
Incomincia il mal di testa ...
L'ordine superiore è comodo per definire
iteratori su tipi di dato polimorfi
fold f (a :: b :: c) x = f c (f b (f a x))
Ora chiediamo a Coq di considerare addn come un simbolo
privo di corpo
Coq è in grado di calcolare in modo simbolico (addn è
momentaneament un simbolo). Ciò significa che il
comportamento di fold non dipende da f e che, nella
lezione 3, potremo dimostrare proprietà di fold valide
per ogni f
Concetti:
ordine superiore
calcolo simbolico
Wrap up
funzioni anonime: fun x : nat => x + 1
tipi di dato: Inductive nat := O | S (n : nat)
pattern matching: match x with O => .. | S p => ..
ricorsione: Fixpoint addn n m := ...
polymorfismo: Inductive pair (A B : Type) := ..
argomenti impliciti: Check pair 2 3
funzioni di ordine superiore e calcolo simbolico
Esercizi
1: Scrivere la funzione anonima
$$ x \mapsto \mathrm{notb}(\mathrm{notb}~x) $$
$$ \mathcal{B} \to \mathcal{B} $$
e verificarne il tipo.
Check (fun (b : bool) => notb (notb b)).
2: Dare il nome neg2 alla funzione precedente
e calcolarla con input true e poi
con input false.
3: Definire il tipo di dato bussola con
costruttori Nord, Sud, Est, Ovest.
Inductive bussola := Nord | Sud | Est | Ovest.
4: Definire la funzione isNord che data una bussola
restituisce il booleano true se e solo se
la direzione è nord
Definition isNord b := match b with Nord => true | _ => false end.
Nota: [_ => ..] significa "in tutti gli altri casi.."
5: Definire la funzione xorb che dati due booleani
restituisce true se e solo se uno dei due è true.
Eseguire la funzione con input true true e false true
Definition xorb b1 b2 :=
match b1 with
| false => b2
| true => notb b2 end.
Eval compute in xorb true true.
Eval compute in xorb false true.
6: Scrivere la funzione ricorsiva eqn che
prende due numeri di tipo nat e restituisce true
se e solo se i due numeri sono uguali. Testare la funzione.
Fixpoint eqn n1 n2 :=
match n1 with
| O => match n2 with O => true | S _ => false end
| S p1 => match n2 with O => false | S p2 => eqn p1 p2 end end.
Eval compute in eqn 3 4.
Eval compute in eqn 3 3.
7: Scrivere la funzione ricorsiva leqn che
prende due numeri di tipo nat e restituisce true
se e solo se il primo numero è minore o uguale al secondo.
Testare la funzione.
Fixpoint leqn n1 n2 :=
match n1 with
| O => true
| S p1 => match n2 with O => false | S p2 => leqn p1 p2 end end.
Eval compute in leqn 3 4.
Eval compute in leqn 4 3.
8: Definire la funzione odd che restituisce true
se e solo se il numero naturale in input è dispari.
Fixpoint odd n :=
match n with
| O => false
| S O => true
| S (S p) => odd p end.
Eval compute in odd 3.
Eval compute in odd 4.
Nota: la sintassi concreta di Coq permette di scrivere
pattern-match profondi. Non è altro che una notazione.
Nel caso predente è espansa in
Fixpoint odd n :=
match n with
| O => false
| S q =>
match q with
| O => true
| S p => odd p end end.
8bis: Definire la funzione swap che prende
una coppia (di tipo prod A B) e restituisce
la coppia dove gli argomenti sono invertiti (di tipo
prod B A.
Definition swap {A B} (x : prod A B) :=
match x with
| pair a b => pair b a
end.
9: Definire la funzione filter che prende
una lista (di tipo A) una funzione di tipo
(A -> bool) e restituisce la lista degli elementi
sui quali la funzione restituisce true.
Testare filter odd (1 :: 2 :: 3 :: nil).
Fixpoint filter {A} (f : A -> bool) l :=
match l with
| nil => nil
| x :: xs =>
let ys := filter f xs in
match f x with
| true => x :: ys
| false => ys end end.
Eval compute in filter odd (1 :: 2 :: 3 :: nil).
Nota: la sintassi [let nome := espressione in ..]
permette di dare un nome a una espression al fine di
non dovere ripetere l'espressione (potenzialmente
grande) in quanto segue.
10: Definire la funzione ricorsiva rev che prende
una lista l (di tipo A) e restituisce la lista
degli elementi di l in ordine inverso.
Nota: è possibile definire una funzione ausiliaria.
Testare la funzione.
Fixpoint rev_aux {A} (l : list A) (acc : list A) :=
match l with
| nil => acc
| x :: xs => rev_aux xs (x :: acc) end.
Definition rev {A} (l : list A) := rev_aux l nil.
Eval compute in rev (1 :: 2 :: 3 :: nil).
11: Definire la funzione rev usando fold.
(Questa volta chiamiamola rev2).
Dichiarare che il primo argomento di rev2 (il tipo A)
è implicito. Testare la funzione.
Definition rev2 {A} (l : list A) := fold cons l nil.
Eval compute in rev2 (1 :: 2 :: 3 :: nil).