Un ciclo invariante è semplicemente qualcosa che è vero su ogni iterazione del ciclo. Ad esempio, prendi un ciclo while
banale:
while x <= 5:
x = x + 1
Qui il ciclo invariante sarebbe quello x ≤ 6
. Ovviamente, nella vita reale, gli invarianti di loop saranno più complicati: trovare il ciclo invariante in generale è qualcosa di un'arte e non può essere fatto facilmente algoritmicamente (per quanto ne so).
Quindi, perché è utile? Bene, a un livello grossolano, è positivo per il debug: se si identifica un invariante importante, è facile verificare che sia valido anche quando si modifica un codice. Potresti semplicemente aggiungere una dichiarazione di asserzione di qualche tipo:
while x <= 5:
x = x + 1
assert x <= 6
Più precisamente, questi invarianti ci aiutano a capire come si comportano i loop. Qui entra in gioco la semantica assiomatica e la logica di Hoare. (Questa parte della risposta è un po 'più avanzata ed esoterica, quindi non preoccuparti troppo.) Nel caso in cui sei arrugginito sulla notazione: ⇒ significa " implica ", ∧ significa" e "e ¬ significa" non ".
L'idea di base è che vogliamo un modo sistematico per dimostrare le proprietà del nostro codice. Il modo in cui ci avviciniamo a questo è guardando le precondizioni e le post-condizioni nel codice. Cioè, vogliamo dimostrare che se alcune condizioni A
contengono prima eseguiamo il nostro codice, alcune condizioni B
contengono dopo lo eseguiamo. Generalmente lo scriviamo come:
{A} code {B}
In generale, questo è piuttosto semplice. Puoi capire intuitivamente come provare qualcosa come {x = 0} x = x + 1 {x = 1}
. Puoi farlo sostituendo x + 1
per x
nella post-condizione, dandoti una formula logica di x = 0 ⇒ x + 1 = 1
che è ovviamente vera. Questo è il modo in cui gestisci il compito in generale: sostituisci semplicemente il nuovo valore per la variabile nella post-condizione.
Altri costrutti come più istruzioni di seguito e istruzioni if sono piuttosto intuitive.
Tuttavia, come lo fai per i loop? Questa è una domanda difficile perché non si conosce (in generale) quante volte un ciclo verrà iterato. È qui che entrano gli invarianti di loop. Stiamo osservando un ciclo come:
while cond: code
Ci sono due possibilità qui. Se cond
è False
, allora è banale: il ciclo non fa nulla, quindi otteniamo solo A ⇒ B
. Ma cosa succede se il ciclo viene effettivamente eseguito? Questo è dove abbiamo bisogno dell'invariant.
L'idea dietro l'invariante è che tiene sempre dentro il loop. Quando sei all'interno del ciclo while, cond
è sempre true. Quindi otteniamo un'affermazione del genere:
{A ∧ cond} code {A}
Questo scrive solo ciò di cui avevamo bisogno formalmente: dato che A
(il ciclo invariante) e cond
si tengono all'inizio del corpo del ciclo, A
deve essere mantenuto alla fine. Se possiamo provarlo per il corpo del ciclo, sappiamo che A
sarà valido indipendentemente dal numero di volte in cui il ciclo viene eseguito. Quindi, data la dichiarazione di cui sopra è vero, possiamo dedurre:
{A} while cond: code {A}
come bonus aggiuntivo, poiché il ciclo while
è appena terminato, sappiamo che cond
deve essere falso. Quindi possiamo effettivamente scrivere il risultato completo come:
{A} while cond: code {A ∧ ¬cond}
Quindi usiamo queste regole per provare qualcosa sul mio esempio sopra. Quello che vogliamo dimostrare è:
{x ≤ 0} while x <= 5: x = x + 1 {x = 6}
Cioè, vogliamo mostrare che se iniziamo con un piccolo x
, alla fine del ciclo x
sarà sempre 6. Questo è piuttosto banale, ma costituisce un buon esempio illustrativo. Quindi il primo passo è trovare un ciclo invariante. In questo caso, l'invariante sarà x ≤ 6
. Ora dobbiamo dimostrare che questo è in realtà un ciclo invariante:
{x ≤ 5 ∧ x ≤ 6} x = x + 1 {x ≤ 6}
Cioè, se x
è minore o uguale a 5, x
è minore o uguale a 6 dopo aver eseguito x = x + 1
. Possiamo farlo usando la regola di sostituzione sopra descritta, ma è comunque abbastanza ovvio.
Quindi, sapendo questo, possiamo dedurre la regola per l'intero ciclo:
{x ≤ 6} while x <= 5: x = x + 1 {x ≤ 6 ∧ ¬(x ≤ 5)}
Quindi questo ci dice che, alla fine del ciclo, x
è sia maggiore che 5 e minore o uguale a 6. Questo semplifica fino a x = 6
. Dal momento che x ≤ 6
ogni volta che x ≤ 0
, abbiamo dimostrato la nostra affermazione iniziale.
Ora, questo potrebbe sembrare un sacco di ostentazione per dimostrare qualcosa di molto ovvio. Dopo tutto, qualsiasi programmatore potrebbe averti detto il valore di x
alla fine di questo ciclo! Tuttavia, l'idea importante è che questo metodo si adatta a cicli più complicati che potrebbero non essere immediatamente evidenti. Ma se riesci a trovare un invariante per tale ciclo, puoi utilizzarlo per dimostrare proprietà più interessanti.
In ogni caso, spero che non sia stato troppo confuso e ti ho dato una buona idea del perché gli invarianti di loop sono importanti ad un livello più fondamentale.