Correttezza dei cicli

Assunzioni

Quando si usano dei cicli, è facile commettere degli errori legati alle assunzioni che vengono fatte. Molto spesso, ci sono delle assunzioni che sono valide all'inizio della prima esecuzione del ciclo, ma che non sono valide alle successive iterazioni. Prendiamo il seguente programma FibonacciErrato.java, che dovrebbe stampare gli elementi della serie di fibonacci.

/*
  La serie di Fibonacci per valori minori di 10000.
*/

class FibonacciErrato {
  public static void main(String[] args) {
    int penultimo, ultimo, nuovo;
    int limite=10000;

    penultimo=1;
    ultimo=1;

    System.out.println(penultimo);
    System.out.println(ultimo);

    while( penultimo+ultimo < limite ) {
      nuovo=penultimo+ultimo;
      System.out.println(nuovo);
    }
  }
}

Eseguendo questo programma, vengono stampati i primi due valori della sequenza 1 e 1, e poi viene stampato ripetutamente il terzo valore 2. Quindi, i primi tre valori che escono sono giusti, ma dal quarto valore in poi si ottengono risultati errati.

L'errore è che le variabili penultimo e ultimo dovrebbero sempre rappresentare il penultimo e l'ultimo elemento trovato della serie. Questo è vero alla prima iterazione del ciclo, dal momento che i primi due valori trovati sono 1 e 1 (e questi sono i valori delle due variabili). A questo punto viene calcolato il nuovo valore nuovo=1+1, e questo è effettivamente il terzo elemento della serie. A questo punto si parte con una nuova iterazione del ciclo. Questa volta però la assunzione che penultimo e ultimo rappresentano gli ultimo due valori trovati non è più valida, dal momento che l'ultimo valore trovato è quello che sta nella variabile nuovo, mentre il valore che sta in ultimo è il penultimo valore trovato. Questo è il motivo per cui il programma non funziona: la assunzione che è vera alla prima iterazione del ciclo non lo è più alle successive iterazioni. Per rendere il programma corretto occorre fare in modo che la assunzione risulti valida anche alle successive iterazioni. Il programma corretto Fibonacci.java modifica il contenuto delle variabili penultimo e ultimo in modo tale che, alla fine di ogni iterazione del ciclo, contenengano gli ultimi due elementi trovati.

Analogamente, nel programma di ricerca di un intervallo che contiene uno zero di una funzione, Nullo.java, si parte dall'assunzione che c,d siano due valori per i quali la funzione ha segno diverso. È quindi necessario che la stessa condizione sia verificata alla fine di ogni singola iterazione del ciclo. Il concetto si può riassumere come segue:

Se nel corpo un ciclo uso una assunzione A, allora:
  • la assunzione A deve essere vera la prima volta che eseguo il ciclo;
  • devo verificare che, assumendo A vera all'inizio del corpo del ciclo, allora A risulta vera anche alla fine.

In genere nei cicli c'è sempre una proprietà che viene mantenuta in tutte le iterazioni, e che è necessaria perché il tutto funzioni. La prima cosa da controllare è che la assunzione sia vera alla prima iterazione del ciclo. Questo risulta di solito abbastanza naturale, ed è quindi difficile trovare errori legati a questo. La seconda cosa che si deve fare è controllare che, assumendo che la assunzione si vera all'inizio di una generica iterazione del ciclo, ed eseguendo il corpo del ciclo, la assunzione alla fine è ancora verificata.

Naturalmente, non è necessario che la assunzione valga anche in mezzo al corpo del ciclo. Per esempio, il fatto che le due variabili contenessere gli ultimi due valori trovati non vale a metà della iterazione (per esempio non vale subito dopo la istruzione System.out.println(nuovo)).

Terminazione

Non è difficile fare esempi di cicli che non terminano. Per esempio, il ciclo

while( 1==1 ) {
  ...
}

viene eseguito all'infinito, dal momento che la condizione è sempre vera (a meno che non ci sia un break all'interno del ciclo).

In alcuni casi, il ciclo infinito è esattamente quello che si vuole. Di solito però si vuole che un programma faccia delle operazioni le sue escuzioni e poi termini, il che implica che tutti i cicli che contiene devono prima o poi terminare. In particolare, se dopo il ciclo ci sono altre istruzioni da eseguire, allora la mancata terminazione fa sí che le istruzioni seguenti non siano eseguite, il che è chiaramente un errore. In generale, i cicli devono sempre terminare.

Nel caso dei cicli for usati per assegnare ripetutamente a una variabile valori crescenti o decrescenti la terminazione di rado è un problema: i cicli for(var=val1; var<=val2; var=var+1) terminano sempre: l'unico problema che può verificarsi è quello di scrivere una condizione con il <= mentre la istruzione che segue è un decremento x=x-1. In altre parole, questi cicli possono non terminare se si scrive per errore una delle due cose seguenti:

Il problema può essere più grave nel caso dei cicli while e dei cicli for generici (cioè quelli di tipo for(istruzione1; condizione; istruzione2) in cui condizioni e istruzioni sono generiche). In questi casi, la verifica di terminazione può essere più complicata. Per esempio, il programma FibonacciErrato.java non termina, e il motivo è che le variabili che si trovano nella condizione di terminazione non vengono modificate dopo la prima iterazione del ciclo (questo è a sua volta dovuto al fatto che una assunzione che veniva usata non era più vera alla fine della prima iterazione).

In generale, verificare se un ciclo termina è difficile. Ci sono però delle cose che è possibile controllare facilmente. Per esempio, all'interno del ciclo deve esistere sempre la possibilità di cambiare le variabili della condizione, e deve essere sempre possibile che queste variabili assumano valori che fanno uscire dal ciclo. Per esempio, se si ha un ciclo while( a!=0 ), e all'interno del ciclo le uniche operazioni che modificano il valore di a sono le assegnazioni a=1 e a=-1, è chiaro che la variabile non assume mai il valore nullo che fa uscire dal ciclo.

Si tenga altresí presente che la istruzione break fa comunque uscire dai cicli. A volte si usano cicli in cui si sa che la condizione è sempre verificata, come per esempio while( 1!=0 ), semplicemente perchè la uscita dal ciclo è garantita da una istruzione break. In questo caso, occorre verificare se è sempre possibile che la istruzione di break venga effettivamente eseguita.