Stefano M. Nicoletti

Lezione 7: Induzione, semplificazione e verifica del software

Registrazione video della Lezione 7

1. Scopo della lezione

Riprendiamo la prima dimostrazione per induzione della Lezione 6. Dimostriamo altre proprietà della somma, introduciamo simp e combiniamo induzione, riscrittura e semplificazione. Applichiamo poi gli stessi strumenti alla verifica di un algoritmo ricorsivo sulle liste.

2. Definizioni e risultati disponibili

Continuiamo a usare i naturali, la somma e il doppio definiti nella Lezione 6:

Naturale ha i costruttori zero e succ. La funzione somma (m n : Naturale) ricorre sul secondo argomento, mentre doppio (n : Naturale) ricorre sull'unico argomento.

Ricordare quale argomento viene analizzato non è un dettaglio puramente sintattico. La definizione di somma può scegliere un ramo del match quando conosce la forma del secondo argomento. Per questo somma m zero e somma m (succ n) si riducono direttamente, mentre somma zero n richiede un ragionamento per induzione quando n è ancora una variabile.

Sono già disponibili le equazioni computazionali e il risultato conclusivo della Lezione 6:

theorem lecture07_somma_zero_destra (m : Naturale) : somma m zero = m := m:Naturale⊢ somma m zero = m All goals completed! 🐙
theorem lecture07_somma_succ_destra (m n : Naturale) : somma m (succ n) = succ (somma m n) := m:Naturalen:Naturale⊢ somma m n.succ = (somma m n).succ All goals completed! 🐙
theorem lecture07_doppio_succ (n : Naturale) : doppio (succ n) = succ (succ (doppio n)) := n:Naturale⊢ doppio n.succ = (doppio n).succ.succ All goals completed! 🐙
theorem lecture07_zero_somma (n : Naturale) : somma zero n = n := n:Naturale⊢ somma zero n = n induction n with ⊢ somma zero zero = zero All goals completed! 🐙 n:Naturaleih:somma zero n = n⊢ somma zero n.succ = n.succ n:Naturaleih:somma zero n = n⊢ (somma zero n).succ = n.succ n:Naturaleih:somma zero n = n⊢ n.succ = n.succ All goals completed! 🐙

I primi tre risultati descrivono direttamente il comportamento delle definizioni. L'ultimo, invece, è un teorema generale ottenuto per induzione. Questa distinzione sarà utile durante tutta la lezione: alcune trasformazioni sono computazioni, altre richiedono risultati già dimostrati.

3. Una seconda dimostrazione per induzione

Vogliamo ora dimostrare che un successore nel primo argomento può essere portato davanti alla somma:

somma (succ m) n = succ (somma m n).

La forma dell'enunciato può suggerire di fare induzione su m, perché il successore compare accanto a quella variabile. Dobbiamo però guardare la definizione della funzione. somma analizza il secondo argomento, e in entrambe le somme quel posto è occupato da n. È quindi n a impedire alla definizione di scegliere tra il caso zero e il caso succ. Facciamo induzione su n.

theorem lecture07_succ_somma (m n : Naturale) : somma (succ m) n = succ (somma m n) := m:Naturalen:Naturale⊢ somma m.succ n = (somma m n).succ induction n with m:Naturale⊢ somma m.succ zero = (somma m zero).succ All goals completed! 🐙 m:Naturalen:Naturaleih:somma m.succ n = (somma m n).succ⊢ somma m.succ n.succ = (somma m n.succ).succ m:Naturalen:Naturaleih:somma m.succ n = (somma m n).succ⊢ (somma m.succ n).succ = (somma m n).succ.succ m:Naturalen:Naturaleih:somma m.succ n = (somma m n).succ⊢ (somma m n).succ.succ = (somma m n).succ.succ All goals completed! 🐙

Nel caso base sostituiamo n con zero. Entrambi i lati si riducono alla stessa espressione e rfl conclude. Nel passo induttivo consideriamo succ n. unfold somma apre entrambe le somme e il goal contiene la stessa uguaglianza richiesta per n. L'ipotesi induttiva ih sostituisce quella sottoespressione, dopo di che i due lati coincidono.

La scelta della variabile d'induzione segue dunque la struttura della computazione. Cerchiamo la variabile che occupa l'argomento analizzato dalla funzione e osserviamo se, nel caso successore, l'assunzione induttiva potrà riscrivere la chiamata ricorsiva ottenuta.

4. Associatività, commutatività e simp

L'associatività afferma che il modo di raggruppare tre addendi non modifica il risultato. Nel nostro enunciato la somma esterna a sinistra ha c come secondo argomento. A destra, c compare come secondo argomento di somma b c, che a sua volta determina la computazione della somma esterna. Scegliamo quindi c come variabile d'induzione.

theorem lecture07_somma_assoc (a b c : Naturale) : somma (somma a b) c = somma a (somma b c) := a:Naturaleb:Naturalec:Naturale⊢ somma (somma a b) c = somma a (somma b c) induction c with a:Naturaleb:Naturale⊢ somma (somma a b) zero = somma a (somma b zero) All goals completed! 🐙 a:Naturaleb:Naturalec:Naturaleih:somma (somma a b) c = somma a (somma b c)⊢ somma (somma a b) c.succ = somma a (somma b c.succ) a:Naturaleb:Naturalec:Naturaleih:somma (somma a b) c = somma a (somma b c)⊢ (somma (somma a b) c).succ = somma a (somma b c.succ) a:Naturaleb:Naturalec:Naturaleih:somma (somma a b) c = somma a (somma b c)⊢ (somma (somma a b) c).succ = somma a (somma b c).succ a:Naturaleb:Naturalec:Naturaleih:somma (somma a b) c = somma a (somma b c)⊢ (somma (somma a b) c).succ = (somma a (somma b c)).succ All goals completed! 🐙

Nel caso zero, le somme che hanno zero come secondo argomento si riducono e rfl conclude. Nel caso succ c eseguiamo tre riscritture con lecture07_somma_succ_destra. La prima apre la somma esterna a sinistra. La seconda apre somma b (succ c) a destra. La terza apre la nuova somma esterna prodotta sul lato destro. A quel punto resta esattamente l'uguaglianza fornita dall'ipotesi induttiva, racchiusa sotto succ, e rw [ih] conclude.

La commutatività afferma invece che scambiare i due addendi non modifica il risultato. Non segue soltanto dalla computazione, perché somma non tratta simmetricamente i due argomenti: analizza il secondo e lascia invariato il primo. Facciamo induzione su b. Nel caso base il lato sinistro si riduce per definizione, mentre sul lato destro usiamo lecture07_zero_somma. Nel passo successore, lecture07_somma_succ_destra tratta il successore a destra e lecture07_succ_somma tratta quello che compare a sinistra dopo lo scambio.

theorem lecture07_somma_comm (a b : Naturale) : somma a b = somma b a := a:Naturaleb:Naturale⊢ somma a b = somma b a induction b with a:Naturale⊢ somma a zero = somma zero a a:Naturale⊢ somma a zero = a All goals completed! 🐙 a:Naturaleb:Naturaleih:somma a b = somma b a⊢ somma a b.succ = somma b.succ a a:Naturaleb:Naturaleih:somma a b = somma b a⊢ (somma a b).succ = somma b.succ a a:Naturaleb:Naturaleih:somma a b = somma b a⊢ (somma a b).succ = (somma b a).succ All goals completed! 🐙

Questa dimostrazione mostra perché costruiamo una raccolta di lemmi piccoli. La definizione fornisce direttamente le equazioni sul secondo argomento. I teoremi zero_somma e succ_somma forniscono le forme corrispondenti sul primo argomento. La commutatività può quindi combinare i due punti di vista.

Quando una dimostrazione richiede molte riscritture elementari, possiamo usare il semplificatore. simp applica equazioni orientate verso forme più semplici e svolge anche computazioni che Lean riconosce direttamente. Non cerca una dimostrazione in modo arbitrario: usa un insieme di regole chiamato simp-set.

Confrontiamo tre modi di usare lo stesso risultato. rw richiede una precisa riscrittura scelta da noi. simp only [lemmi] usa soltanto i lemmi elencati, oltre alle riduzioni essenziali del semplificatore. simp [lemma] usa le regole standard e aggiunge lemma per quel comando.

theorem lecture07_zero_somma_con_rw (n : Naturale) : somma zero n = n := n:Naturale⊢ somma zero n = n All goals completed! 🐙

Nel primo teorema indichiamo esattamente la riscrittura. Nel secondo chiediamo al semplificatore di lavorare con una sola regola dichiarata. Nel terzo aggiungiamo il lemma al simp-set già disponibile per quella singola chiamata. Il risultato è lo stesso, ma cambia il grado di controllo e di automazione.

theorem lecture07_zero_somma_con_simp_only (n : Naturale) : somma zero n = n := n:Naturale⊢ somma zero n = n All goals completed! 🐙
theorem lecture07_zero_somma_con_simp (n : Naturale) : somma zero n = n := n:Naturale⊢ somma zero n = n All goals completed! 🐙
theorem lecture07_succ_somma_con_simp (m n : Naturale) : somma (succ m) n = succ (somma m n) := m:Naturalen:Naturale⊢ somma m.succ n = (somma m n).succ All goals completed! 🐙

L'attributo local simp vale soltanto all'interno della sezione. In questo modo possiamo sperimentare con un insieme di regole senza modificare tutte le dimostrazioni successive del modulo. Nel teorema interno alla sezione basta scrivere simp: il semplificatore trova automaticamente lecture07_succ_somma nel simp-set locale.

@[simp] theorem lecture07_doppio_zero : doppio zero = zero := ⊢ doppio zero = zero All goals completed! 🐙

@[simp] registra una regola per il resto del modulo. Lo usiamo per una regola canonica come doppio zero = zero. Associatività e commutatività non sono regole adatte al simp-set: cambiano soltanto il raggruppamento o l'ordine degli argomenti.

La scelta delle regole è importante. Una buona regola di semplificazione porta in modo prevedibile verso una forma più breve o canonica. Orientare automaticamente l'associatività richiederebbe scegliere sempre un particolare raggruppamento. La commutatività potrebbe invece continuare a scambiare gli argomenti senza produrre una forma evidentemente più semplice. Per questi risultati preferiamo una riscrittura esplicita nel punto in cui serve.

5. Una dimostrazione conclusiva

Combiniamo induzione e riscritture per dimostrare che il doppio di una somma è la somma dei doppi. La sottoespressione che resta bloccata è somma m n, perciò facciamo induzione su n.

theorem lecture07_doppio_somma (m n : Naturale) : doppio (somma m n) = somma (doppio m) (doppio n) := m:Naturalen:Naturale⊢ doppio (somma m n) = somma (doppio m) (doppio n) induction n with m:Naturale⊢ doppio (somma m zero) = somma (doppio m) (doppio zero) All goals completed! 🐙 m:Naturalen:Naturaleih:doppio (somma m n) = somma (doppio m) (doppio n)⊢ doppio (somma m n.succ) = somma (doppio m) (doppio n.succ) m:Naturalen:Naturaleih:doppio (somma m n) = somma (doppio m) (doppio n)⊢ doppio (somma m n).succ = somma (doppio m) (doppio n.succ) m:Naturalen:Naturaleih:doppio (somma m n) = somma (doppio m) (doppio n)⊢ (doppio (somma m n)).succ.succ = somma (doppio m) (doppio n.succ) m:Naturalen:Naturaleih:doppio (somma m n) = somma (doppio m) (doppio n)⊢ (doppio (somma m n)).succ.succ = somma (doppio m) (doppio n).succ.succ m:Naturalen:Naturaleih:doppio (somma m n) = somma (doppio m) (doppio n)⊢ (doppio (somma m n)).succ.succ = (somma (doppio m) (doppio n).succ).succ m:Naturalen:Naturaleih:doppio (somma m n) = somma (doppio m) (doppio n)⊢ (doppio (somma m n)).succ.succ = (somma (doppio m) (doppio n)).succ.succ All goals completed! 🐙

Il caso base segue dalle definizioni di somma e doppio. Nel passo successore, la prima riscrittura porta il successore fuori da somma m (succ n). Le due riscritture con lecture07_doppio_succ descrivono il doppio di un successore sui due lati. Le due successive riscritture della somma espongono la stessa struttura di successori. Infine ih sostituisce il doppio della somma più piccola con la somma dei doppi.

Dopo avere verificato ogni passaggio, possiamo esprimere la stessa dimostrazione con un insieme controllato di regole di semplificazione:

theorem lecture07_doppio_somma_breve (m n : Naturale) : doppio (somma m n) = somma (doppio m) (doppio n) := m:Naturalen:Naturale⊢ doppio (somma m n) = somma (doppio m) (doppio n) induction n with m:Naturale⊢ doppio (somma m zero) = somma (doppio m) (doppio zero) All goals completed! 🐙 m:Naturalen:Naturaleih:doppio (somma m n) = somma (doppio m) (doppio n)⊢ doppio (somma m n.succ) = somma (doppio m) (doppio n.succ) All goals completed! 🐙

La versione abbreviata non esprime un argomento matematico diverso. Usa le stesse equazioni, ma lascia che simp only scelga dove applicarle. Manteniamo la lista esplicita perché, in questa fase, vogliamo sapere quali risultati rendono valida la dimostrazione.

6. Dall'induzione alla verifica del software

Un programma riceve un input e produce un output. Verificarlo significa formulare con precisione una proprietà attesa e dimostrarla per ogni input. Per applicare questa idea a una nuova struttura dati, iniziamo definendo induttivamente il tipo delle liste.

inductive Lista (α : Type) where
  | vuota
  | cons (testa : α) (coda : Lista α)

Una lista è quindi vuota oppure è costruita con un elemento iniziale e una lista più piccola. Nella prossima lezione useremo questa struttura per definire un algoritmo e verificarne alcune proprietà.

Fonti