Làm thế nào để hiểu mã đệ quy này?


12

Tôi đã tìm thấy mã này trong hướng dẫn An Introduction to Programming in Emacs Lispthể hiện đệ quy với sự trợ giúp của condhàm để tìm ra số lượng sỏi dựa trên số hàng đã nhập, tức là nếu hàng = 2, thì sỏi phải là 3, nếu là 4 hàng thì phải là 10 viên sỏi ở đó

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

ước tính thành 10 sau khi vượt qua đối số 4:

(triangle-using-cond 4)

Hướng dẫn không giải thích rõ ràng những gì xảy ra ở mỗi bước trong mã ví dụ này và tôi không thể hiểu được cách thức đệ quy hoạt động ở đây. Bạn có thể vui lòng giúp tôi hiểu từng bước cơ học những gì xảy ra ở mỗi trường hợp không?


Tôi sẽ để người khác giúp bạn với từ "đệ quy" (vì tôi nghĩ nó giống như một cái gì đó khác với ngữ cảnh này) hoặc giải thích rõ hơn những gì tôi sắp viết: (a) nếu số nhỏ hơn hoặc bằng đến 0, rồi 0; (b) nếu số bằng 1, thì 1; (c) nếu số lớn hơn 1, sau đó thêm số vào giá trị được hàm trả về triangle-using-condvới đối số là 1 nhỏ hơn bất kể số đó là gì. Các điều kiện đi theo thứ tự a, b, và sau đó c - bất cứ điều gì khớp trước, là nơi buck dừng lại.
luật

như thông dịch elisp thông thường đánh giá từ trong cùng đến ngoài cùng. Như vậy , 1-4 = 3. Bây giờ cuộc gọi đệ quy sẽ được (triangle-using-cond 3), nhưng điều đó sẽ kết thúc với cùng một cuộc gọi đệ quy lặp đi lặp lại cho đến khi nó chạm vào 1 điều kiện, phải không? chuyện gì sẽ xảy ra tiếp theo?
tiến sĩ

Ồ, tôi hiểu rồi - chức năng tự sử dụng lại ở bước 3 - được rồi, điểm tốt.
luật

Tôi tự hỏi điều gì sẽ là kết quả của (triangle-using-cond 3)?
tiến sĩ

2
nb Hàm 1-này có một tên đặc biệt dễ gây hiểu lầm, đặc biệt nếu bạn đọc một cuộc gọi như thể đó là ký hiệu infix. Nó trả về đối số của nó trừ đi một; KHÔNG một trừ đi đối số.
phils

Câu trả lời:


14

Sử dụng "gỡ lỗi printf"

Bạn có thể để Emacs giúp bạn hiểu bằng cách sửa đổi định nghĩa hàm:

(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))))))

Chỉ cần thêm (message ...)một nơi nào đó để có một dấu vết được in vào*Messages* bộ đệm.

Sử dụng Edebug

Đặt điểm bất cứ nơi nào trong định nghĩa hàm và nhấn C-u C-M-xvào "công cụ" nó. Sau đó đánh giá chức năng, ví dụ bằng cách đặt điểm sau (triangle-using-cond 3)và nhấn C-x C-e.

Bây giờ bạn đang ở chế độ Edebug. Nhấn thanh không gian để bước qua chức năng. Các giá trị trung gian của mỗi biểu thức được hiển thị trong vùng echo. Để thoát chế độ Edebug chỉ cần nhấn q. Để xóa thiết bị, đặt điểm ở bất kỳ đâu trong định nghĩa và nhấn C-M-xđể đánh giá lại định nghĩa.

Sử dụng trình gỡ lỗi Emacs tiêu chuẩn

M-x debug-on-entry triangle-using-cond, sau đó, khi triangle-using-condđược gọi, bạn được đặt trong trình gỡ lỗi Emacs (bộ đệm *Backtrace*).

Bước qua đánh giá bằng cách sử dụng d(hoặc cbỏ qua mọi đánh giá không thú vị).

Để xem trạng thái trung gian (giá trị biến, v.v.) bạn có thể sử dụng e bất cứ lúc nào. Bạn được nhắc nhập sexp để đánh giá và kết quả đánh giá được in.

Trong khi bạn sử dụng trình gỡ lỗi, hãy giữ một bản sao của mã nguồn hiển thị trong một khung khác, để bạn có thể theo dõi những gì đang diễn ra.

Bạn cũng có thể chèn các cuộc gọi rõ ràng để vào trình gỡ lỗi (nhiều hoặc ít điểm dừng) tại các vị trí tùy ý trong mã nguồn. Bạn chèn (debug)hoặc (debug nil SOME-SEXP-TO-EVALUATE). Trong trường hợp sau, khi trình gỡ lỗi được nhập SOME-SEXP-TO-EVALUATEđược đánh giá và kết quả được in. (Hãy nhớ rằng bạn có thể chèn mã đó vào mã nguồn và sử dụng C-M-xđể đánh giá nó, sau đó hoàn tác - bạn không cần lưu tệp đã chỉnh sửa.)

Xem hướng dẫn Elisp, nút Using Debuggerđể biết thêm thông tin.

Đệ quy như một vòng lặp

Dù sao, hãy nghĩ về đệ quy như một vòng lặp. Có hai trường hợp chấm dứt được xác định: (<= number 0)(= number 1). Trong những trường hợp này, hàm trả về một số đơn giản.

Trong trường hợp đệ quy, hàm trả về tổng của số đó và kết quả của hàm với number - 1. Cuối cùng, hàm sẽ được gọi với một 1hoặc một số nhỏ hơn hoặc bằng 0.

Do đó, kết quả trường hợp đệ quy là:

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

Lấy ví dụ (triangle-using-cond 4). Hãy tích lũy biểu thức cuối cùng:

  • trong lần lặp đầu tiên number4, vì vậy (> number 1)nhánh được theo sau. Chúng ta bắt đầu xây dựng một biểu thức (+ 4 ...và gọi hàm với (1- 4), tức là (triangle-using-cond 3).

  • bây giờ number3, và kết quả là (+ 3 (triangle-using-cond 2)). Biểu thức tổng kết quả là (+ 4 (+ 3 (triangle-using-cond 2))).

  • number2hiện nay, vì vậy biểu thức là(+ 4 (+ 3 (+ 2 (triangle-using-cond 1))))

  • number1bây giờ, và chúng tôi lấy (= number 1)chi nhánh, kết quả là nhàm chán 1. Toàn bộ biểu hiện là (+ 4 (+ 3 (+ 2 1))). Đánh giá rằng từ trong ra ngoài và bạn nhận được: (+ 4 (+ 3 3)), (+ 4 6), hoặc chỉ 10.


3
Edebug sẽ còn tốt hơn nữa. =)
Malabarba

Làm thế nào để có được dấu vết được in bằng cách sử dụng message (...), nhấn C-x C-echỉ hiển thị kết quả cuối cùng (10) không có gì khác? Tui bỏ lỡ điều gì vậy?
tiến sĩ

@Malabarba, làm thế nào để Edebughành động?
tiến sĩ

1
@doctorate nhấn C-u C-M-xvới điểm bên trong hàm để edebug nó. Sau đó chỉ cần chạy chức năng như bình thường.
Malabarba

@doctorate các (message ...)công cụ in vào *Message*bộ đệm.
rekado

6

Mô hình thay thế cho ứng dụng thủ tục từ SICP có thể giải thích thuật toán để hiểu mã như thế này.

Tôi đã viết một số mã để tạo điều kiện này là tốt. lispy-flattentừ gói lispy làm điều này. Đây là kết quả của việc áp dụng lispy-flattencho (triangle-using-cond 4):

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

Bạn có thể đơn giản hóa biểu thức trên thành:

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

Sau đó làm phẳng một lần nữa:

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

Kết quả cuối cùng:

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

3

Điều này không cụ thể đối với Emacs / Elisp, nhưng nếu bạn có nền tảng toán học, thì đệ quy giống như cảm ứng toán học . (Hoặc nếu bạn không: sau đó khi bạn học cảm ứng, nó giống như đệ quy!)

Hãy bắt đầu với định nghĩa:

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

Khi number4, không ai trong số hai điều kiện đầu tiên tổ chức, vì vậy nó được đánh giá theo điều kiện thứ ba:
(triangle-using-cond 4)được đánh giá là
(+ number (triangle-using-cond (1- number))), cụ thể là như
(+ 4 (triangle-using-cond 3)).

Tương tự,
(triangle-using-cond 3)được đánh giá là
(+ 3 (triangle-using-cond 2)).

Tương tự, (triangle-using-cond 2)được đánh giá là
(+ 2 (triangle-using-cond 1)).

Nhưng đối với (triangle-using-cond 1), điều kiện thứ hai giữ, và nó được đánh giá là 1.

Một lời khuyên cho bất cứ ai học đệ quy: cố gắng tránh

lỗi bắt đầu phổ biến của việc cố gắng nghĩ về những gì xảy ra trong cuộc gọi đệ quy thay vì tin rằng cuộc gọi đệ quy hoạt động (đôi khi được gọi là bước nhảy vọt của đức tin).

Nếu bạn đang cố gắng thuyết phục bản thân liệu (triangle-using-cond 4)sẽ trả lời đúng câu trả lời, chỉ cần giả sử rằng (triangle-using-cond 3)sẽ trả lời đúng và xác minh xem câu trả lời đó có đúng trong trường hợp đó không. Tất nhiên bạn phải xác minh trường hợp cơ sở quá.


2

Các bước tính toán cho ví dụ của bạn sẽ như sau:

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

Điều kiện 0 thực sự không bao giờ được đáp ứng vì 1 là đầu vào đã kết thúc đệ quy.


(1)không phải là một biểu thức hợp lệ.
rekado

1
Nó đánh giá tốt với M-x calc. :-) Nghiêm túc mà nói, tôi muốn thể hiện sự tính toán chứ không phải đánh giá Lisp.
paprika

Ồ, tôi thậm chí không nhận thấy rằng nó (4 +thay vì (+ 4trong câu trả lời của bạn ... :)
rekado

0

Tôi nghĩ nó khá dễ, bạn không cần emacs để làm điều này, nó chỉ là toán tiểu học.

f (0) = 0

f (1) = 1

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

nên f (5) = 5 + f (4) = 5 + 4 + f (3) = 5 + 4 + 3 + 2 + 1 + 0

Bây giờ thì rõ ràng rồi.


Tuy nhiên, trong trường hợp của hàm này, f (0) không bao giờ được gọi.
rekado
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.