Como entender esse código de recursão?

12

Eu encontrei este código no manual An Introduction to Programming in Emacs Lispdemonstrando recursão com a ajuda da condfunção para descobrir o número de seixos com base no número digitado de linhas, ou seja, se linhas = 2, então seixos devem ser 3, se 4 linhas, então devem ser 10 seixos há.

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

avalie para 10 após passar o argumento 4:

(triangle-using-cond 4)

O manual não explicou claramente o que acontece em cada etapa deste código de exemplo em particular e não consegui descobrir como a recursão funciona aqui. Você pode me ajudar a entender a mecânica passo a passo, o que acontece em cada instância?

doutorado
fonte
Deixarei que outra pessoa o ajude com a palavra "recursão" (porque acho que é algo diferente do que neste contexto) ou melhor explique o que estou prestes a escrever: (a) se o número for menor ou igual para 0, depois 0; (b) se o número for igual a 1, então 1; (c) se o número for maior que 1, adicione número ao valor retornado pela função triangle-using-condcom o argumento 1 menor que qualquer que seja o número. As condições vão na ordem de a, bec, e c - o que corresponder primeiro, é onde o dinheiro para.
lawlist
como de costume, o intérprete elisp avalia do mais interno para o mais externo. Assim 1-4 = 3,. Agora a chamada recursiva será (triangle-using-cond 3), mas isso terminará com a mesma chamada recursiva repetidamente até atingir o 1 condicional, certo? O que vai acontecer à seguir?
doctorate
Ah, entendo - a função se reutiliza na etapa 3 - tudo bem, bom ponto.
lawlist
Eu me pergunto o que seria o resultado (triangle-using-cond 3)?
doctorado
2
NOTA: A função 1-tem um nome particularmente enganador, especialmente se você ler uma chamada como se fosse uma notação infix. Retorna seu argumento menos um; NÃO menos um argumento.
Phs #

Respostas:

14

Usando "depuração printf"

Você pode deixar o Emacs ajudá-lo a entender, modificando a definição da função:

(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 adicionar um (message ...)lugar para imprimir uma trilha no *Messages*buffer.

Usando Edebug

Coloque o ponto em qualquer lugar dentro da definição da função e pressione C-u C-M-x"instrumentar". Em seguida, avalie a função, por exemplo, colocando o ponto depois (triangle-using-cond 3)e pressionando C-x C-e.

Agora você está no modo Edebug. Pressione a barra de espaço para percorrer a função. Os valores intermediários de cada expressão são mostrados na área de eco. Para sair do modo Edebug, basta pressionar q. Para remover a instrumentação, coloque um ponto em qualquer lugar dentro da definição e pressione C-M-xpara reavaliar a definição.

Usando o depurador Emacs padrão

M-x debug-on-entry triangle-using-cond, quando triangle-using-condfor chamado, você será colocado no depurador Emacs (buffer *Backtrace*).

Passe pela avaliação usando d(ou cpara pular as avaliações desinteressantes).

Para ver o estado intermediário (valores variáveis, etc.), você pode usar a equalquer momento. Você é solicitado a inserir um sexp para avaliar e o resultado da avaliação é impresso.

Enquanto você usa o depurador, mantenha uma cópia do código-fonte visível em outro quadro, para poder acompanhar o que está acontecendo.

Você também pode inserir chamadas explícitas para inserir o depurador (mais ou menos pontos de interrupção) em locais arbitrários no código-fonte. Você insere (debug)ou (debug nil SOME-SEXP-TO-EVALUATE). No último caso, quando o depurador é inserido, SOME-SEXP-TO-EVALUATEé avaliado e o resultado é impresso. (Lembre-se de que você pode inserir esse código no código-fonte e usá C-M-x-lo para avaliá-lo e desfazer - você não precisa salvar o arquivo editado.)

Consulte o manual do Elisp, nó Using Debuggerpara obter mais informações.

Recursão como um loop

Enfim, pense em recursão como um loop. Existem dois casos de rescisão definidos: (<= number 0)e (= number 1). Nesses casos, a função retorna um número simples.

No caso recursivo, a função retorna a soma desse número e o resultado da função com number - 1. Eventualmente, a função será chamada com um 1ou um número menor ou igual a zero.

O resultado do caso recursivo é, portanto:

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

Tome por exemplo (triangle-using-cond 4). Vamos acumular a expressão final:

  • na primeira iteração numberé 4, então a (> number 1)ramificação é seguida. Começamos a construir uma expressão (+ 4 ...e chamamos a função com (1- 4), ie (triangle-using-cond 3).

  • agora numberé 3e o resultado é (+ 3 (triangle-using-cond 2)). A expressão total do resultado é (+ 4 (+ 3 (triangle-using-cond 2))).

  • numberé 2agora, então a expressão é(+ 4 (+ 3 (+ 2 (triangle-using-cond 1))))

  • numberé 1agora, e tomamos o (= number 1)ramo, resultando em um tédio 1. Toda a expressão é (+ 4 (+ 3 (+ 2 1))). Avaliar que de dentro para fora e você tem: (+ 4 (+ 3 3)), (+ 4 6), ou apenas 10.

rekado
fonte
3
Edebug será ainda melhor. =)
Malabarba
como obter a trilha impressa usando o message (...), pressionando C-x C-eapenas mostra o resultado final (10) nada mais? Estou esquecendo de algo?
doctorado
@ Malabarba, como entrar Edebugem ação?
doutorado
1
@doctorate bateu C-u C-M-xcom point dentro da função para edebug. Em seguida, basta executar a função normalmente.
Malabarba 6/11
@doctorate o (message ...)material imprime no *Message*buffer.
rekado
6

O Modelo de Substituição para Aplicação de Procedimentos do SICP pode explicar o algoritmo para entender códigos como este.

Eu escrevi algum código para facilitar isso também. lispy-flattendo pacote lispy faz isso. Aqui está o resultado da aplicação lispy-flattende (triangle-using-cond 4):

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

Você pode simplificar a expressão acima para apenas:

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

Em seguida, aplainar mais uma vez:

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

O resultado final:

(+ 4 (+ 3 (+ 2 1)))
abo-abo
fonte
3

Isso não é específico para o Emacs / Elisp, mas se você tem formação em matemática, a recursão é como indução matemática . (Ou, se você não aprender: então, quando você aprende indução, é como recursão!)

Vamos começar com a definição:

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

Quando numberé 4, nenhuma das duas primeiras condições é válida, portanto é avaliada de acordo com a terceira condição:
(triangle-using-cond 4)é avaliada como
(+ number (triangle-using-cond (1- number))), a saber, como
(+ 4 (triangle-using-cond 3)).

Da mesma forma,
(triangle-using-cond 3)é avaliado como
(+ 3 (triangle-using-cond 2)).

Da mesma forma, (triangle-using-cond 2)é avaliado como
(+ 2 (triangle-using-cond 1)).

Mas para (triangle-using-cond 1), a segunda condição é válida e é avaliada como 1.

Um conselho para quem está aprendendo recursão: tente evitar

o erro comum dos iniciantes em tentar pensar no que acontece durante a chamada recursiva, em vez de confiar que a chamada recursiva funciona (às vezes chamada de salto recursivo da fé).

Se você está tentando se convencer de (triangle-using-cond 4)que retornará a resposta correta, basta supor que (triangle-using-cond 3)ela retornará a resposta correta e verifique se ela estará correta nesse caso. Claro que você também deve verificar o caso base.

ShreevatsaR
fonte
2

As etapas de cálculo para o seu exemplo seriam as seguintes:

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

A condição 0 nunca é atendida porque 1 como entrada já encerra a recursão.

páprica
fonte
(1)não é uma expressão válida.
rekado
1
Ele avalia muito bem com M-x calc. :-) Falando sério, pretendia mostrar o cálculo, não a avaliação do Lisp.
Paprika
Oh, eu nem percebi que é (4 +em vez de (+ 4em sua resposta ... :)
rekado
0

Eu acho que é bem fácil, você não precisa do emacs lisp, é apenas matemática do ensino fundamental.

f (0) = 0

f (1) = 1

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

então f (5) = 5 + f (4) = 5 + 4 + f (3) = 5 + 4 + 3 + 2 + 1 + 0

Agora é óbvio.

Chen Bin
fonte
No caso desta função, porém, f (0) nunca é chamado.
rekado