Dipende da quanto rigorosamente definisci "ricorsione".
Se lo richiediamo rigorosamente per coinvolgere lo stack di chiamate (o qualsiasi altro meccanismo utilizzato per mantenere lo stato del programma), allora possiamo sempre sostituirlo con qualcosa che non lo fa. In effetti, i linguaggi che portano naturalmente a un uso intensivo della ricorsione tendono ad avere compilatori che fanno un uso pesante dell'ottimizzazione delle chiamate di coda, quindi ciò che scrivi è ricorsivo ma ciò che esegui è iterativo.
Ma consideriamo un caso in cui effettuiamo una chiamata ricorsiva e utilizziamo 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 la prima chiamata ricorsiva iterativa è facile:
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 rimuovere il goto
per allontanare i velociraptor 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 archiviare 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 che cosa è stato compilato, abbiamo trasformato il codice che utilizza lo stack di chiamate per implementare la ricorsione in codice che non lo fa (e nel fare ciò il codice trasformato che genererà un'eccezione di overflow dello stack per valori anche abbastanza piccoli in codice che semplicemente impiegare un tempo tremendamente lungo per tornare [vedi Come posso evitare che la mia funzione Ackerman trabocchi lo stack? per alcune ulteriori ottimizzazioni che lo fanno effettivamente tornare per molti più input possibili]).
Considerando come la ricorsione è 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 quel metodo come ricorsivo, allora ci sono davvero cose di cui non possiamo farne a meno. Generalmente sebbene non etichettiamo tale codice ricorsivo. Il termine ricorsione è utile perché copre un certo insieme di approcci e ci dà un modo di parlarne, e non ne stiamo più usando uno.
Ovviamente, tutto questo presuppone che tu abbia una scelta. Esistono sia lingue che vietano le chiamate ricorsive, sia lingue prive delle strutture cicliche necessarie per iterare.