2012-06-02 13 views
17

Sto provando a invertire in modo ricorsivo una lista, ma sto ottenendo Can only recur from tail position sulla corsa. Cosa significa esattamente e come può essere migliorato il mio codice in modo che funzioni?Clojure: si può ripetere solo dalla posizione di coda

(defn recursive-reverse [coll] 
    (loop [coll coll] 
    (if (< (count coll) 2) '(coll) 
     (conj (first coll) (recur (rest coll))) 
    ))) 

EDIT

uscita per soluzione di Oscar. Funziona per le liste ma non per i vettori?

user=> (= (recursive-reverse [1 2 3 4 5]) (recursive-reverse '(1 2 3 4 5))) 
false 
user=> (= '(1 2 3 4 5) [1 2 3 4 5]) 
true 
user=> (recursive-reverse [1 2 3 4 5]) 
[1 2 3 4 5] 
user=> (recursive-reverse '(1 2 3 4 5)) 
(5 4 3 2 1) 
+1

funziona in modo diverso per elenchi e vettori perché 'conj' funziona in modo diverso per entrambi. È più economico "aggiungere" l'elemento nella parte anteriore di un elenco e alla fine di un vettore. 'conj' esamina il tipo di collezione e la aggiunge nel modo più economico. Ad essere onesti, questo non mi è mai sembrato giusto, ma sono sicuro che Rich Hickey e gli altri sviluppatori di Clojure ci hanno pensato molto e hanno deciso che i pro superano ogni contro. – Gert

+0

Affascinante. Immagino che la performance sia stata la loro proprietà numero uno per le operazioni di base, ma sì sembra strano. – Chris

+2

Spot on, @Gert. Ho aggiornato la mia risposta, preferirei restituire la risposta corretta di un tipo diverso rispetto a una risposta errata se l'input è un vettore –

risposta

19

L'errore Can only recur from tail position significa che non stai chiamando recur come l'ultima espressione nella parte ricorsiva della funzione - infatti, nel codice conj è l'ultima espressione.

Alcuni miglioramenti per rendere il lavoro codice:

  • chiedere se la raccolta è vuota come il caso base, invece di confrontare se la sua lunghezza è inferiore a due
  • conj riceve una collezioneper la sua primo parametro, non un elemento
  • È un'idea migliore usare cons anziché conj (che aggiunge nuovi elementi in luoghi diversi a seconda del tipo di calcestruzzo della raccolta, in base allo documentation).In questo modo, la raccolta restituita verrà annullata se la raccolta di input è una lista o un vettore (sebbene il tipo della raccolta restituita sarà sempre
  • un elenco con un singolo elemento (il simbolo coll) e non la raccolta effettiva
  • Per invertire correttamente un elenco è necessario scorrere l'elenco di input e aggiungere ciascun elemento all'inizio di un elenco di output; utilizzare un parametro accumulatore per questo
  • Per sfruttare la ricorsione in coda, chiamare recur nella posizione di coda della funzione; in questo modo ogni invocazione ricorsiva prende una quantità costante di spazio e lo stack non crescerà senza limiti

Credo che questo sia quello che volevamo ottenere:

(defn recursive-reverse [coll] 
    (loop [coll coll 
     acc (empty coll)] 
     (if (empty? coll) 
      acc 
      (recur (rest coll) (cons (first coll) acc))))) 
+2

Grazie per la risposta eccezionale. Nuovo mistero: perché funziona con i vettori ma non con le liste? Ho inserito il mio output 'leon repl' nella domanda. – Chris

+2

Succede a causa del modo in cui ['conj'] (http://clojuredocs.org/clojure_core/clojure.core/conj) funziona:" L''aggiunta' può avvenire in "luoghi" diversi a seconda del tipo concreto ". Se cambiamo 'conj' per' cons' (come ho appena fatto nella mia risposta) la sequenza restituita sarà sempre invertita, ma il tipo della collezione sarà 'clojure.lang.Cons'. Date un'occhiata a questo [post] (http://stackoverflow.com/a/3009747/201359) per una spiegazione –

6

È possibile richiamare solo dalla posizione di coda in Clojure. Fa parte del design della lingua ed è una restrizione relativa alla JVM.

È possibile chiamare il nome della funzione (utilizzando la ricorsione) senza ricorrere, e in base alla struttura del programma, ad esempio se si utilizzano sequenze pigri o meno, è possibile che non si verifichi un aumento dello stack. Ma è meglio usare ricorrere, e l'uso di ricorrenza con ricorrenza ti consente alcuni collegamenti locali.

Ecco un esempio da 4Clojure.com dove la ricorsione viene utilizzata senza ricorrere.

(fn flt [coll] 
    (let [l (first coll) r (next coll)] 
    (concat 
     (if (sequential? l) 
     (flt l) 
     [l]) 
     (when (sequential? r) 
     (flt r))))) 
2

Il modo più semplice per rendere il codice il lavoro è quello di utilizzare il nome della funzione, invece di recur:

(defn recursive-reverse [coll] 
    (loop [coll coll] 
    (if (< (count coll) 2) '(coll) 
     (conj (first coll) (recursive-reverse (rest coll))) 
    ))) 

si farà lo stack di chiamate enorme e errore fuori per i grandi ingressi abbastanza, naturalmente.

Ecco un altro modo di scrivere una versione tail-call-friendly di recursive-reverse:

(defn recursive-reverse [s] 
    (letfn [ 
    (my-reverse [s c] 
     (if (empty? s) 
     c 
     (recur (rest s) (conj c (first s)))))] 
    (my-reverse s '()))) 

Si tira voci fuori la testa della lista di input e li aggiunge alla testa della lista dell'accumulatore.

'Posizione di coda' significa solo che lo recur è l'ultima operazione eseguita dalla funzione prima che ritorni. Questo differisce dalla tua soluzione di "ricorsione naturale" in cui c'è ancora lavoro nella funzione tra la chiamata e il ritorno.

+1

perché avete usato 'letfn' invece di' loop' con due argomenti? Usare 'loop' è più idiomatico. – Gert

+1

@Gert: è solo un altro modo per farlo, ho pensato che se l'OP non avesse familiarità con il ciclo allora letfn avrebbe avuto più senso. c'è una fonte per il giudizio idiomatico? la mia comprensione è stata che scrivere questo con un accumulatore non era idiomatico (basato su qualcosa in Joy of Clojure) comunque. –

+0

questo è un buon punto. Ma 'loop' esiste esattamente per questo caso (creando un punto di ricorsione che non è la stessa chiamata di funzione), è anche più conciso, ampiamente utilizzato e illustrato/documentato su http: // clojure.org/special_forms – Gert

Problemi correlati