Rimozione della ricorsione - uno sguardo alla teoria dietro le quinte


10

Sono nuovo di questo sito e questa domanda non è certo a livello di ricerca, ma vabbè. Ho un piccolo background nell'ingegneria del software e quasi nessuno in CSTheory, ma lo trovo attraente. Per farla breve, vorrei una risposta più dettagliata a quanto segue se questa domanda è accettabile su questo sito.

Quindi, so che ogni programma ricorsivo ha un analogo iterativo e capisco la spiegazione popolare che gli viene offerta mantenendo qualcosa di simile allo "stack di sistema" e spingendo le impostazioni dell'ambiente come l'indirizzo di ritorno ecc. Trovo questo tipo di handwavy .

Essendo un po 'più concreto, vorrei (formalmente) vedere come si dimostra questa affermazione nei casi in cui si ha una funzione che invoca la catena . Inoltre, cosa succede se ci sono alcune istruzioni condizionali che potrebbero portare un F ho effettuare una chiamata a un certo F j ? Cioè, il potenziale grafico di chiamata di funzione ha alcuni componenti fortemente connessi.F0F1FiFi+1FnF0FiFj

Vorrei sapere come possono essere gestite queste situazioni, diciamo un convertitore ricorsivo a iterativo. E la descrizione fatta a mano a cui ho fatto riferimento in precedenza, è davvero sufficiente per questo problema? Intendo allora perché trovo facile rimuovere la ricorsione in alcuni casi. In particolare, rimuovere la ricorsione dall'attraversamento del pre-ordine di un albero binario è davvero facile: è una domanda standard di intervista, ma rimuovere la ricorsione in caso di ordine postale è sempre stato un incubo per me.

Quello che sto davvero chiedendo sono domande2

(1) Esiste davvero una prova più formale (convincente?) Che la ricorsione può essere convertita in iterazione?

(2) Se questa teoria è davvero in circolazione , allora perché trovo, ad esempio, più facile l' iterazione del preordine e il postordine così difficile? (diverso dalla mia intelligenza limitata)


1
come la parola iteratizzante :)
Akash Kumar,

non sono sicuro di aver compreso appieno, ma se la ricorsione finisce da qualche parte, puoi effettivamente simulare uno stack di sistema usando il tuo stack. Per la parte (2), i problemi non sono diversi in termini di complessità computazionale.
singhsumit,

Penso che questa domanda sarebbe stata la più adatta per il sito di Informatica che non è ancora attivo . Per quanto riguarda la tua seconda domanda, puoi spiegare perché pensi che sia più difficile? Il processo dovrebbe essere quasi identico.
Raffaello,

grazie a tutti per i vostri commenti - suppongo di avere abbastanza da leggere.
Itachi Uchiha,

@Raphael - Un commento sul motivo per cui penso che l'iterazione del postorder sia difficile (oltre a me non essere in grado di farlo). Stavo leggendo alcuni articoli sulla rimozione della ricorsione e mi sono imbattuto in qualcosa chiamato funzioni ricorsive della coda. Si scopre che sono più facili da iteratizzare. Non capisco ancora formalmente perché sia ​​vero; ma c'è un'altra cosa che dovrei aggiungere. Ho sentito che il postorder iteratizzante richiede due stack e non uno, ma non conosco i dettagli. E ora mi sono perso - perché questa differenza tra queste due modalità trasversali? E perché la ricorsione della coda è facile da gestire?
Itachi Uchiha,

Risposte:


6

Se ho capito bene, sei chiaro sulla conversione di funzioni che non contengono altre chiamate di funzioni se non a se stesse.

FF1FnFF1,,FnF

FjFFjFFj

f(0) = a
f(n) = f'(g(n-1))

g(0) = b
g(n) = g'(f(n-1))

con f' e g'le funzioni non ricorsive (o almeno indipendenti da fe g) diventano

h(0) = (a,b)
h(n) = let (f,g) = h(n-1) in (f'(g), g'(f)) end

f(n) = let (f, _) = h(n) in f end
g(n) = let (_, g) = h(n) in g end

Ciò si estende naturalmente a più funzioni coinvolte e funzioni più complicate.


Sono contento di aver potuto aiutare. Ricorda di accettare la tua risposta preferita facendo clic sul segno di spunta accanto ad essa.
Raffaello,

1
Raphel, il tuo trucco funziona solo quando entrambe le funzioni ricorsive accettano argomenti dello stesso tipo. Se fe gaccetta diversi tipi di tipi, è necessario un trucco più generale.
Andrej Bauer,

@AndrejBauer buona osservazione, mi è totalmente mancato. mi è piaciuto molto l'approccio di Raffaello, ma come hai osservato in casi generali, probabilmente abbiamo bisogno di un'idea diversa. Puoi dare altri suggerimenti?
Itachi Uchiha,

fgn1n2

Bene, vedi la mia risposta su come farlo.
Andrej Bauer,

8

Sì, ci sono ragioni convincenti per credere che la ricorsione possa essere trasformata in iterazione. Questo è ciò che fa ogni compilatore quando traduce il codice sorgente in linguaggio macchina. Per la teoria dovresti seguire i suggerimenti di Dave Clarke. Se desideri vedere il codice effettivo che converte la ricorsione in codice non ricorsivo, dai un'occhiata machine.mlnel linguaggio MiniML nel mio PL Zoo (nota che illoop funzione in basso, che in realtà esegue il codice, è ricorsiva di coda e quindi può essere banalmente convertito in un loop reale).

Un'altra cosa. MiniML non supporta funzioni reciprocamente ricorsive. Ma questo non è un problema. Se si ha una ricorsione reciproca tra le funzioni

f1:A1B1
f2:A2B2
fn:AnBn

la ricorsione può essere espressa in termini di una singola mappa ricorsiva

f:A1++AnB1++Bn,

8

Potresti voler guardare la macchina SECD . Un linguaggio funzionale (sebbene possa essere qualsiasi linguaggio) viene tradotto in una serie di istruzioni che gestiscono cose come mettere argomenti di pile, "invocare" nuove funzioni e così via, il tutto gestito da un semplice ciclo.
Le chiamate ricorsive non vengono mai effettivamente invocate. Invece, le istruzioni del corpo della funzione chiamata vengono posizionate nello stack per l'esecuzione.

Un approccio correlato è la macchina CEK .

Entrambi sono in circolazione da molto tempo, quindi c'è un sacco di lavoro là fuori su di loro. E naturalmente ci sono prove del loro funzionamento e la procedura per "compilare" un programma in istruzioni SECD è lineare nelle dimensioni del programma (non deve pensare al programma).

Il punto della mia risposta è che esiste una procedura automatica per fare ciò che vuoi. Sfortunatamente, la trasformazione non sarà necessariamente in termini di cose che sono immediatamente facili da interpretare per un programmatore. Penso che la chiave sia che quando si desidera iterizzare un programma, è necessario memorizzare nello stack ciò che il programma deve fare quando si ritorna da una chiamata di funzione iterizzata (questa è chiamata continuazione). Per alcune funzioni (come le funzioni ricorsive della coda) la continuazione è banale. Per altri la continuazione potrebbe essere molto complessa, specialmente se devi codificarla da solo.


sarò onesto qui. Voglio davvero capire perché (e come) puoi iterare ogni programma ricorsivo. Ma trovo difficile leggere un documento - di solito non sono accessibili a me. Voglio dire, voglio una ragione più profonda della descrizione "handwavy" di cui ho parlato nella domanda. ma sono anche contento di qualcosa che mi dà nuove intuizioni - non deve essere la prova completa nei suoi dettagli nitidi e grintosi
Itachi Uchiha,

[CNT] Voglio dire, mi piacerà la prova, se ce n'è una, per dirmi perché l'iterazione di un programma è più facile dell'altro. Ma in un certo senso, il convertitore ricorsivo a iterativo dovrebbe funzionare indipendentemente dal programma ricorsivo che assume come input. Non è sicuro, ma immagino che realizzare un tale convertitore potrebbe essere duro quanto il problema di arresto? Sto solo indovinando qui - ma mi piacerebbe che esistesse un convertitore ricorsivo a iterativo e, in caso affermativo, mi piacerebbe che spiegasse la complessità intrinseca dell'iterizzazione di diversi programmi ricorsivi. non sono sicuro, ma devo modificare la domanda? La mia domanda è chiara?
Itachi Uchiha,

@ItachiUchiha - Non credo che il tuo problema sia indecidibile. Guarda la risposta di Andrej Bauer. Nota che ogni compilatore lo fa quando traduce il codice sorgente in linguaggio macchina. Inoltre aggiunge che puoi vedere il codice reale che converte ricorsivo in non ricorsivo nella lingua MiniM (a) l. Ciò indica chiaramente che esiste una procedura decisionale per "iterare" la ricorsione. Non sono sicuro della difficoltà / complessità intrinseca (concettuale) della rimozione della ricorsione. Non capisco questa domanda molto chiaramente ma sembra interessante. Forse puoi modificare la tua domanda per ottenere una risposta migliore
Akash Kumar,

Il punto della mia risposta è che esiste una procedura automatica per fare ciò che vuoi. Sfortunatamente, la trasformazione non sarà necessariamente in termini di cose che sono immediatamente facili da interpretare per un programmatore. Penso che la chiave sia che quando si desidera iterizzare un programma, è necessario memorizzare nello stack ciò che il programma deve fare quando si ritorna da una chiamata di funzione iterizzata (questa è chiamata continuazione). Per alcune funzioni (come le funzioni ricorsive della coda) la continuazione è banale. Per altri la continuazione potrebbe essere molto complessa, specialmente se devi codificarla da solo.
Dave Clarke,

6

D : "Esiste davvero una prova più formale (convincente?) Che la ricorsione può essere convertita in iterazione?"

A : La completezza Turing di una macchina Turing :-)

Scherzi a parte, il modello di macchina RASP (Random Access) del programma memorizzato equivalente di Turing è vicino al funzionamento dei microprocessori reali e il suo set di istruzioni contiene solo un salto condizionale (nessuna ricorsione). La possibilità di auto-modificare dinamicamente il codice semplifica il compito di implementare subroutine e chiamate ricorsive.

Penso che si possano trovare molti articoli / articoli sulla " conversione ricorsiva a iterativa " (vedi la risposta di Dave o solo le parole chiave di Google), ma forse un approccio meno noto (e pratico ) è l'ultima ricerca sull'implementazione hardware di algoritmi ricorsivi ( utilizzando il linguaggio VHDL che viene "compilato" direttamente in un componente hardware). Ad esempio, vedi l'articolo di V.Sklyarov " Implementazione basata su FPGA di algoritmi ricorsivi " ( L'articolo suggerisce un nuovo metodo per implementare algoritmi ricorsivi nell'hardware .... Sono state studiate due applicazioni pratiche di algoritmi ricorsivi nell'area di ordinamento e compressione dei dati in dettaglio .... ).


1

Se hai familiarità con le lingue che supportano lambda, una strada è quella di esaminare la trasformazione CPS. Rimuovere l'utilizzo dello stack di chiamate (e in particolare la ricorsione) è esattamente ciò che fa la trasformazione CPS. Trasforma un programma contenente chiamate di procedura in un programma con solo chiamate di coda (puoi pensare a queste come goto, che è un costrutto iterativo).

La trasformazione CPS è strettamente correlata al mantenimento esplicito di uno stack di chiamate in uno stack basato su array tradizionale, ma anziché in un array lo stack di chiamate è rappresentato con chiusure collegate.


0

a mio avviso, questa domanda risale alle origini delle definizioni di calcolo ed è stata molto tempo fa provata rigorosamente in quel periodo in cui il calcolo lambda della chiesa (che cattura fortemente il concetto di ricorsione) si dimostrò equivalente alle macchine di Turing, ed è contenuto nella terminologia ancora utilizzata "lingue / funzioni ricorsive". inoltre apparentemente un riferimento chiave successivo in questo senso è il seguente

Come sottolineato dall'articolo di Peter Landin del 1965 A Corrispondenza tra ALGOL 60 e la notazione Lambda della Chiesa, i linguaggi di programmazione procedurale sequenziale possono essere compresi in termini di calcolo lambda, che fornisce i meccanismi di base per l'astrazione procedurale e l'applicazione della procedura (sottoprogramma).

gran parte del bkd su questo è in questa pagina di Wikipedia tesi di chiesa-turing . non sono sicuro dei dettagli esatti, ma l'articolo di Wikipedia sembra indicare che fu Rosser (1939) a provare questa equivalenza tra il calcolo lambda e le macchine di turing. forse / presumibilmente il suo documento ha un meccanismo simile a una pila per convertire le chiamate lambda (forse ricorsive) alla costruzione di tm?

Rosser, JB (1939). "Un'esposizione informale di prove del teorema di Godel e del teorema della Chiesa". The Journal of Symbolic Logic (The Journal of Symbolic Logic, Vol. 4, No. 2) 4 (2): 53–60. DOI: 10,2307 / 2.269.059. JSTOR 2269059.

nota ovviamente per chiunque sia interessato ai principi che il moderno linguaggio Lisp e il Variant Scheme hanno intenzionalmente una forte somiglianza con il calcolo lambda. lo studio del codice dell'interprete per la valutazione delle espressioni porta a idee originariamente contenute in articoli per la completezza turing del calcolo lambda.


1
La prova di equivalenza di Turing / lambda si trova nell'appendice di questo documento: www.cs.virginia.edu/~robins/Turing_Paper_1936.pdf
Radu GRIGore,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.