Dall'iterazione alla ricorsione
Descrizione del problema
In Haskell, lo strumento fondamentale per la realizzazione di
algoritmi iterativi è la ricorsione. Tuttavia, vi sono casi in
cui la versione ricorsiva di un algoritmo iterativo non è immediata
da individuare oppure è obiettivamente inefficiente. Un tipico
esempio è la funzione che calcola il
In netto contrasto, una classica realizzazione imperativa/iterativa
della funzione di Fibonacci ha complessità lineare in
Un altro esempio di algoritmo iterativo che non ammette un’ovvia
versione ricorsiva è quello che determina se un certo numero
In questo esempio, la difficoltà nell’individuare una versione
ricorsiva dell’algoritmo è dovuta al fatto che la primalità di un
certo numero
In questo caso di studio vedremo come ottenere realizzazioni ricorsive con complessità lineare della funzione di Fibonacci e del test di primalità. La tecnica illustrata è generale e può essere usata per trasformare algoritmi imperativi/iterativi in funzionali/ricorsivi.
Soluzione ricorsiva lineare di Fibonacci
Osserviamo che l’implementazione Java della funzione di Fibonacci
agisce su uno stato che comprende, oltre al parametro fibonacci
otteniamo una funzione
fibonacciAux
definita come segue:
Abbiamo chiamato questa funzione fibonacciAux
e non fibonacci
per sottolineare il fatto che tale funzione non risolve il problema
originario, bensì uno più generale. Più precisamente, se indichiamo
con
per ogni
Se
Notiamo che il caso base di fibonacciAux
corrisponde alla
condizione di terminazione del ciclo while
nella versione Java: se
k
è 0 il risultato è m
. Dualmente, il caso ricorsivo di
fibonacciAux
corrisponde all’esecuzione di una iterazione del
ciclo while
: se k
è maggiore di 0, allora il risultato è
ottenuto ponendo m
a n
, n
a m + n
e k
a k - 1
. Queste
modifiche di m
, n
e k
sono ottenute nella versione Java con
assegnamenti che modificano lo stato. Nella versione Haskell
otteniamo un effetto analogo applicando ricorsivamente
fibonacciAux
alle versioni modificate di m
, n
e k
. Tra
l’altro, in questo modo non dobbiamo preoccuparci del fatto che m
ed n
devono essere modificate con un gioco di prestigio di somme e
sottrazioni oppure usando una terza variabile di appoggio.
Resta il fatto che fibonacciAux
non è la funzione che
risolve il problema originario, ma possiamo ottenere fibonacci
come semplice specializzazione di fibonacciAux
:
Il modo in cui fibonacciAux
è applicata corrisponde
all’inizializzazione di m
ed n
nella versione Java. Possiamo
ulteriormente raffinare questa soluzione osservando che l’utilità
della funzione ausiliaria fibonacciAux
è limitata alla funzione
fibonacci
. Per tale motivo ha senso definire fibonacciAux
come
funzione locale di fibonacci
, in un blocco di codice indentato
che segue la parola chiave where
:
Definire localmente la funzione ausiliaria ci consente, tra le altre cose, di darle un nome più conciso senza rischiare conflitti con altre funzioni omonime, e rende meno importante documentarla con una annotazione di tipo esplicita, che dunque omettiamo.
Soluzione ricorsiva del test di primalità
Come per fibonacci
, anche l’implementazione Java del test di
primalità agisce su uno stato che comprende il parametro
Notiamo in particolare che la funzione ausiliaria aux
può accedere
al parametro n
della funzione primo
, all’interno della quale è
localmente definita. Notiamo inoltre la solita corrispondenza tra
casi base di aux
e condizioni di terminazione del ciclo nella
versione Java di primo
. Il ciclo termina se k >= n
, nel qual
caso abbiamo testato tutti i candidati divisori di n
senza
trovarne alcuno, e dunque restituiamo True
o False
a seconda
che k
sia uguale o maggiore di n
, rispettivamente. Il ciclo
termina anche se k
divide n
, il che significa che abbiamo
trovato un divisore di n
e dunque possiamo concludere che n
non
è primo. Il caso induttivo della ricorsione corrisponde a una
iterazione del ciclo, in cui ci limitiamo ad aggiornare k
al
candidato successivo. Proprio come nel caso della funzione di
Fibonacci, anche qui l’applicazione aux 2
, che dà il via alla
ricorsione, corrisponde all’inizializzazione di k
nella versione
Java dell’algoritmo.
Esercizi
- Scrivere una funzione ricorsiva corrispondente al seguente algoritmo
iterativo:
- Scrivere una funzione ricorsiva corrispondente al seguente algoritmo
iterativo:
- Scrivere una funzione ricorsiva corrispondente al seguente algoritmo
iterativo: