Come capire questo codice ricorsivo?


12

Ho trovato questo codice nel manuale che An Introduction to Programming in Emacs Lispdimostra la ricorsione con l'aiuto della condfunzione per scoprire il numero di ciottoli in base al numero di righe immesso, ovvero se righe = 2, allora i ciottoli dovrebbero essere 3, se 4 righe quindi dovrebbero essere 10 ciottoli Là.

(defun triangle-using-cond (number)
  (cond ((<= number 0) 0)
        ((= number 1) 1)
        ((> number 1)
         (+ number (triangle-using-cond (1- number))))))

valuta 10 dopo aver superato l'argomento 4:

(triangle-using-cond 4)

Il manuale non ha spiegato chiaramente cosa succede in ogni fase di questo codice di esempio in particolare e non sono riuscito a capire come funziona la ricorsione qui. Potete aiutarmi a capire passo dopo passo la meccanica che cosa succede in ogni istanza?


Lascerò che qualcun altro ti aiuti con la parola "ricorsione" (perché penso che sia qualcosa di diverso rispetto a questo contesto) o spiegherò meglio cosa sto per scrivere: (a) se il numero è inferiore o uguale a 0, quindi 0; (b) se il numero è uguale a 1, quindi 1; (c) se il numero è maggiore di 1, quindi aggiungere il numero al valore restituito dalla funzione triangle-using-condcon l'argomento 1 in meno di qualunque sia il numero. Le condizioni vanno nell'ordine di a, b, e quindi c - qualunque cosa corrisponda per prima, è dove il dollaro si ferma.
elenco delle leggi

come sempre l'interprete elisp valuta dal più interno al più esterno. Così, 1-4 = 3. Ora la chiamata ricorsiva sarà (triangle-using-cond 3), ma finirà sempre con la stessa chiamata ricorsiva fino a quando non raggiunge il 1 condizionale, giusto? cosa succederà dopo?
dottorato

Oh, capisco - la funzione si riutilizza al passaggio 3 - ok, buon punto.
elenco delle leggi

Mi chiedo quale sarebbe il risultato (triangle-using-cond 3)?
dottorato

2
nb La funzione 1-ha un nome particolarmente fuorviante, soprattutto se si legge una chiamata come se fosse una notazione infissa. Restituisce il suo argomento meno uno; NON uno meno l'argomento.
phils,

Risposte:


14

Utilizzo di "debug printf"

È possibile lasciare che Emacs ti aiuti a capire modificando la definizione della funzione:

(defun triangle-using-cond (number)
  (message (format "called with %d" number))
  (cond ((<= number 0) 0)
        ((= number 1) 1)
        ((> number 1)
         (+ number (triangle-using-cond (1- number))))))

Basta aggiungere un (message ...)posto per avere una traccia stampata nel *Messages*buffer.

Usando Edebug

Posiziona il punto in qualsiasi punto all'interno della definizione della funzione e premi C-u C-M-xper "strumento". Quindi valutare la funzione, ad esempio posizionando il punto dopo (triangle-using-cond 3)e colpendo C-x C-e.

Ora sei in modalità Edebug. Premi la barra spaziatrice per scorrere la funzione. I valori intermedi di ciascuna espressione sono mostrati nell'area dell'eco. Per uscire dalla modalità Edebug basta premere q. Per rimuovere la strumentazione, posiziona un punto qualsiasi all'interno della definizione e premi C-M-xper rivalutare la definizione.

Utilizzo del debugger standard di Emacs

M-x debug-on-entry triangle-using-cond, quindi, quando triangle-using-condviene invocato, vieni inserito nel debugger (buffer *Backtrace*) di Emacs .

Passa attraverso la valutazione usando d(o cper saltare tutte le valutazioni poco interessanti).

Per visualizzare lo stato intermedio (valori variabili, ecc.) È possibile utilizzare in equalsiasi momento. Viene richiesto di inserire un sexp per valutare e il risultato della valutazione viene stampato.

Mentre usi il debugger, mantieni una copia del codice sorgente visibile in un altro frame, in modo da poter seguire quello che sta succedendo.

È inoltre possibile inserire chiamate esplicite per immettere il debugger (più o meno punti di interruzione) in posizioni arbitrarie nel codice sorgente. Inserisci (debug)o (debug nil SOME-SEXP-TO-EVALUATE). In quest'ultimo caso, quando viene inserito il debugger SOME-SEXP-TO-EVALUATEviene valutato e il risultato viene stampato. (Ricorda che puoi inserire tale codice nel codice sorgente e utilizzarlo C-M-xper valutarlo, quindi annullare: non è necessario salvare il file modificato.)

Vedere il manuale Elisp, nodo Using Debuggerper ulteriori informazioni.

Ricorsione come un ciclo

Ad ogni modo, pensa alla ricorsione come a un ciclo. Sono definiti due casi di risoluzione: (<= number 0)e (= number 1). In questi casi la funzione restituisce un numero semplice.

Nel caso ricorsivo la funzione restituisce la somma di quel numero e il risultato della funzione con number - 1. Alla fine, la funzione verrà chiamata con uno 1o un numero minore o uguale a zero.

Il risultato del caso ricorsivo è quindi:

(+ number (+ (1- number) (+ (1- (1- number)) ... 1)

Prendi ad esempio (triangle-using-cond 4). Accumuliamo l'espressione finale:

  • nella prima iterazione numberè 4, quindi il (> number 1)ramo è seguito. Iniziamo a costruire un'espressione (+ 4 ...e chiamiamo la funzione con (1- 4), ad es (triangle-using-cond 3).

  • ora numberè 3e il risultato è (+ 3 (triangle-using-cond 2)). L'espressione del risultato totale è (+ 4 (+ 3 (triangle-using-cond 2))).

  • numberè 2ora, quindi l'espressione è(+ 4 (+ 3 (+ 2 (triangle-using-cond 1))))

  • numberè 1ora e prendiamo il (= number 1)ramo, risultando noioso 1. L'intera espressione è (+ 4 (+ 3 (+ 2 1))). Valutare che dall'interno verso l'esterno e si ottiene: (+ 4 (+ 3 3)), (+ 4 6), o semplicemente 10.


3
Edebug sarà ancora meglio. =)
Malabarba,

come ottenere la traccia stampata usando il message (...), colpire C-x C-emostra solo il risultato finale (10) nient'altro? Mi sto perdendo qualcosa?
dottorato

@Malabarba, come mettere Edebugin atto?
dottorato il

1
@doctorate ha colpito C-u C-M-xcon un punto all'interno della funzione per edebug. Quindi esegui la funzione normalmente.
Malabarba,

@doctorate le (message ...)cose stampate sul *Message*buffer.
rekado,

6

Il modello sostitutivo per l'applicazione della procedura SICP può spiegare l'algoritmo per comprendere il codice in questo modo.

Ho scritto del codice per facilitare anche questo. lispy-flattendal pacchetto lispy fa questo. Ecco il risultato dell'applicazione lispy-flattena (triangle-using-cond 4):

(cond ((<= 4 0)
       0)
      ((= 4 1)
       1)
      ((> 4 1)
       (+ 4 (triangle-using-cond (1- 4)))))

Puoi semplificare l'espressione sopra semplicemente:

(+ 4 (triangle-using-cond 3))

Quindi appiattire ancora una volta:

(+ 4 (cond ((<= 3 0)
            0)
           ((= 3 1)
            1)
           ((> 3 1)
            (+ 3 (triangle-using-cond (1- 3))))))

Il risultato finale:

(+ 4 (+ 3 (+ 2 1)))

3

Questo non è specifico di Emacs / Elisp, ma se si ha un background matematico, la ricorsione è come l'induzione matematica . (O se non lo fai: quando impari l'induzione, è come una ricorsione!)

Cominciamo con la definizione:

(defun triangle-using-cond (number)
  (cond ((<= number 0) 0)
        ((= number 1) 1)
        ((> number 1)
         (+ number (triangle-using-cond (1- number))))))

Quando numberè 4, nessuna delle prime due condizioni è valida, quindi viene valutata in base alla terza condizione:
(triangle-using-cond 4)viene valutata come
(+ number (triangle-using-cond (1- number))), cioè come
(+ 4 (triangle-using-cond 3)).

Allo stesso modo,
(triangle-using-cond 3)viene valutato come
(+ 3 (triangle-using-cond 2)).

Allo stesso modo, (triangle-using-cond 2)viene valutato come
(+ 2 (triangle-using-cond 1)).

Ma per (triangle-using-cond 1), la seconda condizione vale, ed è valutata come 1.

Un consiglio per chiunque stia imparando la ricorsione: cerca di evitare

l'errore comune per i principianti nel cercare di pensare a ciò che accade durante la chiamata ricorsiva invece di fidarsi che la chiamata ricorsiva funzioni (a volte chiamata salto ricorsivo della fede).

Se stai cercando di convincerti se (triangle-using-cond 4)restituirà la risposta corretta, supponi solo che (triangle-using-cond 3)restituirà la risposta giusta e verifica se sarà corretta in quel caso. Ovviamente devi verificare anche il caso base.


2

I passaggi di calcolo per il tuo esempio sarebbero i seguenti:

(4 +               ;; step 1
   (3 +            ;; step 2
      (2 +         ;; step 3
         (1))))    ;; step 4
=> 10

La condizione 0 in realtà non è mai soddisfatta perché 1 come input termina già la ricorsione.


(1)non è un'espressione valida.
rekado,

1
Valuta bene con M-x calc. :-) Scherzi a parte, intendevo mostrare il calcolo, non la valutazione Lisp.
paprika,

Oh, non ho nemmeno notato che è (4 +invece (+ 4nella tua risposta ... :)
rekado

0

Penso che sia abbastanza facile, non hai bisogno di emacs lisp sotto questo, è solo la matematica della scuola elementare.

f (0) = 0

f (1) = 1

f (n) = f (n-1) + n quando n> 1

quindi f (5) = 5 + f (4) = 5 + 4 + f (3) = 5 + 4 + 3 + 2 + 1 + 0

Adesso è ovvio.


Nel caso di questa funzione, tuttavia, f (0) non viene mai chiamato.
rekado
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.