È un modo generico per convertire qualsiasi procedura ricorsiva in ricorsione della coda?


13

Sembra che abbia trovato un modo generico per convertire qualsiasi procedura ricorsiva in ricorsione in coda:

  1. Definire una procedura secondaria di supporto con un parametro "risultato" aggiuntivo.
  2. Applicare ciò che verrebbe applicato al valore restituito della procedura a quel parametro.
  3. Chiamare questa procedura di supporto per iniziare. Il valore iniziale per il parametro "risultato" è il valore per il punto di uscita del processo ricorsivo, in modo che il processo iterativo risultante inizi da dove inizia il processo ricorsivo a ridursi.

Ad esempio, ecco la procedura ricorsiva originale da convertire ( esercizio 1.17 SICP ):

(define (fast-multiply a b)
  (define (double num)
    (* num 2))
  (define (half num)
    (/ num 2))
  (cond ((= b 0) 0)
        ((even? b) (double (fast-multiply a (half b))))
        (else (+ (fast-multiply a (- b 1)) a))))

Ecco la procedura convertita, ricorsiva della coda ( esercizio SICP 1.18 ):

(define (fast-multiply a b)
  (define (double n)
    (* n 2))
  (define (half n)
    (/ n 2))
  (define (multi-iter a b product)
    (cond ((= b 0) product)
          ((even? b) (multi-iter a (half b) (double product)))
          (else (multi-iter a (- b 1) (+ product a)))))
  (multi-iter a b 0))

Qualcuno può provare o smentire questo?


1
Primo pensiero: questo potrebbe funzionare per tutte le funzioni singolarmente ricorsive, ma sarei sorpreso se funzionasse per funzioni che effettuano più chiamate ricorsive, dal momento che ciò implicherebbe, ad esempio, che potresti implementare quicksort senza bisogno dello stack spazio. (Le implementazioni efficienti esistenti di quicksort generalmente effettuano 1 chiamata ricorsiva nello stack e trasformano l'altra chiamata ricorsiva in una chiamata di coda che può essere (manualmente o automaticamente) trasformata in un ciclo.)O(logn)
j_random_hacker

Secondo pensiero: la scelta bdi avere una potenza di 2 mostra che inizialmente l'impostazione productsu 0 non è del tutto corretta; ma cambiarlo in 1 non funziona quando bè dispari. Forse hai bisogno di 2 diversi parametri dell'accumulatore?
j_random_hacker,

3
Non hai davvero definito una trasformazione di una definizione ricorsiva non di coda, l'aggiunta di alcuni parametri di risultato e l'utilizzo per l'accumulo è piuttosto vago e difficilmente generalizza a casi più complessi, ad esempio traversate di alberi, in cui hai due chiamate ricorsive. Esiste però un'idea più precisa di "continuazione", in cui si fa parte del lavoro, e quindi si accetta una funzione di "continuazione", ricevendo come parametro il lavoro svolto finora. Si chiama continuazione passando lo stile (cps), vedi en.wikipedia.org/wiki/Continuation-passing_style .
Ariel,

4
Queste diapositive fsl.cs.illinois.edu/images/d/d5/CS422-Fall-2006-13.pdf contengono una descrizione della trasformazione cps, in cui si prende qualche espressione arbitraria (possibilmente con definizioni di funzione con chiamate non di coda) e trasformalo in un'espressione equivalente con solo le chiamate di coda.
Ariel,

@j_random_hacker Sì, vedo che la mia procedura "convertita" è in realtà sbagliata ...
nalzok,

Risposte:


12

La tua descrizione del tuo algoritmo è davvero troppo vaga per valutarla a questo punto. Ma qui ci sono alcune cose da considerare.

CPS

In effetti, esiste un modo per trasformare qualsiasi codice in un modulo che utilizza solo le chiamate di coda. Questa è la trasformazione CPS. CPS ( Continuation-Passing Style ) è una forma di espressione del codice passando ogni funzione una continuazione. Una continuazione è una nozione astratta che rappresenta "il resto di una valutazione". Nel codice espresso in formato CPS, il modo naturale di reificare una continuazione è come una funzione che accetta un valore. In CPS, anziché una funzione che restituisce un valore, applica invece la funzione che rappresenta la continuazione corrente all'essere "restituito" dalla funzione.

Ad esempio, considerare la seguente funzione:

(lambda (a b c d)
  (+ (- a b) (* c d)))

Questo potrebbe essere espresso in CPS come segue:

(lambda (k a b c d)
  (- (lambda (v1)
       (* (lambda (v2)
            (+ k v1 v2))
          a b))
     c d))

È brutto e spesso lento, ma presenta alcuni vantaggi:

  • La trasformazione può essere completamente automatizzata. Quindi non è necessario scrivere (o vedere) il codice in formato CPS.
  • Combinato con thunking e trampolining , può essere utilizzato per fornire l'ottimizzazione della coda in lingue che non forniscono l'ottimizzazione della coda. (L'ottimizzazione della chiamata di coda delle funzioni direttamente ricorsive della coda può essere realizzata con altri mezzi, come convertire la chiamata ricorsiva in un ciclo. Ma la ricorsione indiretta non è così banale da convertire in questo modo.)
  • Con CPS, le continuazioni diventano oggetti di prima classe. Poiché le continuazioni sono l'essenza del controllo, ciò consente praticamente a qualsiasi operatore di controllo di essere implementato come libreria senza richiedere alcun supporto speciale dal linguaggio. Ad esempio, goto, eccezioni e threading cooperativo possono essere tutti modellati utilizzando le continuazioni.

TCO

Mi sembra che l'unica ragione per occuparsi della ricorsione della coda (o delle chiamate in coda in generale) sia ai fini dell'ottimizzazione delle chiamate in coda (TCO). Quindi penso che una domanda migliore da porsi sia "il mio codice di rendimento di trasformazione è ottimizzabile per le chiamate in coda?".

Se consideriamo ancora una volta CPS, una delle sue caratteristiche è che il codice espresso in CPS è costituito esclusivamente da chiamate di coda. Poiché tutto è un richiamo, non è necessario salvare un punto di ritorno nello stack. Quindi tutto il codice in formato CPS deve essere ottimizzato per la coda, giusto?

Bene, non proprio. Vedi, mentre potrebbe sembrare che abbiamo eliminato lo stack, tutto ciò che abbiamo fatto è semplicemente cambiare il modo in cui lo rappresentiamo. Lo stack fa ora parte della chiusura che rappresenta una continuazione. Quindi CPS non rende magicamente ottimizzato tutto il nostro codice di coda.

Quindi, se CPS non può fare tutto il TCO, c'è una trasformazione specifica per la ricorsione diretta che può? No, non in generale. Alcune ricorsioni sono lineari, ma altre no. Le ricorsioni non lineari (ad es. Albero) devono semplicemente mantenere una quantità variabile di stato da qualche parte.


è un po 'confuso quando nella sottosezione " TCO ", quando si dice "coda ottimizzata" si intende in realtà "con un uso costante della memoria". Il fatto che l'utilizzo dinamico della memoria non sia costante non nega il fatto che le chiamate siano effettivamente in coda e non vi sia alcuna crescita illimitata nell'uso dello stack . SICP chiama tali calcoli "iterativi", quindi dicendo "sebbene sia TCO, non lo rende ancora iterativo" avrebbe potuto essere una formulazione migliore (per me).
Will Ness,

@WillNess Abbiamo ancora uno stack di chiamate, è rappresentato in modo diverso. La struttura non cambia solo perché stiamo usando l'heap, piuttosto che lo stack hardware . Dopotutto, ci sono molte strutture di dati basate sulla memoria dinamica dell'heap che hanno "stack" nel loro nome.
Nathan Davis,

l'unico punto qui è che alcune lingue hanno limiti fissi all'utilizzo dello stack di chiamate.
Will Ness, il
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.