Dipende da quanto rigorosamente definisci "ricorsione".
Se lo richiediamo rigorosamente per coinvolgere lo stack di chiamate (o qualunque meccanismo per mantenere lo stato del programma), possiamo sempre sostituirlo con qualcosa che non lo è. Infatti, i linguaggi che portano naturalmente ad un uso intensivo della ricorsione tendono ad avere compilatori che fanno un uso massiccio dell'ottimizzazione di coda, quindi ciò che scrivi è ricorsivo, ma quello che viene eseguito è iterativo.
Ma consideriamo un caso in cui facciamo una chiamata ricorsiva e usiamo il risultato di una chiamata ricorsiva per quella chiamata ricorsiva.
public static BigInteger Ackermann(BigInteger m, BigInteger n)
{
if (m == 0)
return n+1;
if (n == 0)
return Ackermann(m - 1, 1);
else
return Ackermann(m - 1, Ackermann(m, n - 1));
}
Rendere semplice la prima chiamata ricorsiva è semplice:
public static BigInteger Ackermann(BigInteger m, BigInteger n)
{
restart:
if (m == 0)
return n+1;
if (n == 0)
{
m--;
n = 1;
goto restart;
}
else
return Ackermann(m - 1, Ackermann(m, n - 1));
}
Possiamo quindi ripulire rimuovendo goto
per respingere velociraptors e l'ombra di Dijkstra:
public static BigInteger Ackermann(BigInteger m, BigInteger n)
{
while(m != 0)
{
if (n == 0)
{
m--;
n = 1;
}
else
return Ackermann(m - 1, Ackermann(m, n - 1));
}
return n+1;
}
Ma per rimuovere le altre chiamate ricorsive dovremo memorizzare i valori di alcune chiamate in uno stack:
public static BigInteger Ackermann(BigInteger m, BigInteger n)
{
Stack<BigInteger> stack = new Stack<BigInteger>();
stack.Push(m);
while(stack.Count != 0)
{
m = stack.Pop();
if(m == 0)
n = n + 1;
else if(n == 0)
{
stack.Push(m - 1);
n = 1;
}
else
{
stack.Push(m - 1);
stack.Push(m);
--n;
}
}
return n;
}
Ora, quando consideriamo il codice sorgente, abbiamo sicuramente trasformato il nostro metodo ricorsivo in uno iterativo.
Considerando ciò che è stato compilato, abbiamo trasformato il codice che usa lo stack di chiamate per implementare la ricorsione in codice che non lo fa (e così facendo trasforma il codice che genererà un'eccezione di overflow dello stack anche per valori piuttosto piccoli in codice ciò richiederà semplicemente un lungo periodo di tempo estremamente lungo da restituire [vedi Come posso evitare che la funzione Ackerman trabocchi dallo stack? per ulteriori ottimizzazioni che in realtà ritorna per molti più input possibili]).
Considerando il modo in cui la ricorsione viene implementata in generale, abbiamo trasformato il codice che utilizza lo stack di chiamate in codice che utilizza uno stack diverso per contenere le operazioni in sospeso. Potremmo quindi sostenere che è ancora ricorsivo, se considerato a quel livello basso.
E a quel livello, non ci sono davvero altri modi per aggirarlo. Quindi, se consideri tale metodo come ricorsivo, allora ci sono davvero cose che non possiamo fare senza di esso. Generalmente però non etichettiamo tale codice ricorsivo. Il termine ricorsione è utile perché copre un certo insieme di approcci e ci dà modo di parlarne, e non ne usiamo più uno.
Naturalmente, tutto questo presuppone che tu abbia una scelta. Ci sono due lingue che proibiscono le chiamate ricorsive e le lingue che non hanno le strutture di loop necessarie per iterare.