จะเข้าใจรหัสการเรียกซ้ำได้อย่างไร


12

ฉันพบรหัสนี้ในคู่มือที่An Introduction to Programming in Emacs Lispแสดงการเรียกซ้ำด้วยความช่วยเหลือของcondฟังก์ชันเพื่อหาจำนวนของก้อนกรวดตามจำนวนแถวที่ป้อนเช่นถ้าแถว = 2 จากนั้นก้อนกรวดควรเป็น 3 ถ้า 4 แถวก็ควรเป็น 10 ก้อนกรวด ที่นั่น

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

ประเมินถึง 10 หลังจากผ่านการโต้แย้ง 4:

(triangle-using-cond 4)

คู่มือไม่ได้อธิบายอย่างชัดเจนว่าเกิดอะไรขึ้นในแต่ละขั้นตอนของรหัสตัวอย่างนี้โดยเฉพาะและฉันไม่สามารถทราบได้ว่าการเรียกใช้ซ้ำเกิดขึ้นที่นี่ได้อย่างไร คุณกรุณาช่วยฉันเข้าใจกลไกทีละขั้นตอนว่าเกิดอะไรขึ้นในแต่ละตัวอย่าง?


ฉันจะให้คนอื่นช่วยคุณด้วยคำว่า "การเรียกซ้ำ" (เพราะฉันคิดว่ามันเป็นสิ่งที่แตกต่างจากในบริบทนี้) หรืออธิบายสิ่งที่ฉันกำลังจะเขียนดีกว่า: (ก) ถ้าจำนวนน้อยกว่าหรือเท่ากับ ถึง 0 จากนั้น 0; (b) ถ้า number เท่ากับ 1 ดังนั้น 1 (c) ถ้า number มากกว่า 1 ให้เพิ่ม number ไปที่ค่าที่ส่งคืนโดยฟังก์ชันtriangle-using-condโดยอาร์กิวเมนต์คือ 1 น้อยกว่าจำนวนที่เป็น เงื่อนไขเป็นไปตามลำดับ a, b และ c - สิ่งที่ตรงกันก่อนคือที่เจ้าชู้หยุด
ฏหมาย

เช่นล่าม elisp ปกติประเมินจากภายในสุดถึงสุด ดังนั้น1-4 = 3. ตอนนี้การเรียกซ้ำจะเป็น(triangle-using-cond 3)เช่นนั้น แต่จะจบลงด้วยการเรียกซ้ำซ้ำอีกครั้งและอีกครั้งจนกว่าจะถึง 1 เงื่อนไขใช่ไหม? จะเกิดอะไรขึ้นต่อไป
ปริญญาเอก

โอ้ฉันเข้าใจแล้ว - ฟังก์ชั่นนำกลับมาใช้ใหม่ในขั้นตอนที่ 3 - โอเคจุดดี
ฏหมาย

ฉันสงสัยว่าผลลัพธ์จะเป็น(triangle-using-cond 3)อย่างไร
ปริญญาเอก

2
ฟังก์ชั่น1-มีชื่อที่ทำให้เข้าใจผิดโดยเฉพาะอย่างยิ่งโดยเฉพาะอย่างยิ่งถ้าคุณอ่านการโทรราวกับว่ามันเป็นสัญกรณ์มัด มันคืนค่าอาร์กิวเมนต์ลบหนึ่ง ไม่มีใครลบการโต้แย้ง
Phils

คำตอบ:


14

การใช้ "การดีบัก printf"

คุณสามารถให้ Emacs ช่วยให้คุณเข้าใจได้โดยการแก้ไขนิยามฟังก์ชัน:

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

เพียงเพิ่ม(message ...)บางแห่งเพื่อให้พิมพ์เส้นทางไปยัง*Messages*บัฟเฟอร์

ใช้ Edebug

วางจุดใดก็ได้ภายในนิยามฟังก์ชั่นแล้วกดC-u C-M-xเพื่อ "เครื่องดนตรี" จากนั้นประเมินผลการทำงานเช่นการวางจุดหลังและกดปุ่ม(triangle-using-cond 3)C-x C-e

ตอนนี้คุณอยู่ในโหมด Edebug กดแป้นเว้นวรรคเพื่อไปยังฟังก์ชัน ค่ากลางของแต่ละนิพจน์จะแสดงในพื้นที่ echo เพื่อออกจากโหมด Edebug qเพียงแค่กด ในการลบเครื่องมือออกวางจุดใดก็ได้ในคำจำกัดความแล้วกดC-M-xเพื่อประเมินความหมายอีกครั้ง

การใช้ดีบักเกอร์ Emacs มาตรฐาน

M-x debug-on-entry triangle-using-condจากนั้นเมื่อtriangle-using-condมีการเรียกใช้คุณจะอยู่ในดีบักเกอร์ Emacs (บัฟเฟอร์*Backtrace*)

ผ่านการประเมินผลโดยใช้d(หรือcเพื่อข้ามการประเมินผลที่ไม่น่าสนใจ)

หากต้องการดูสถานะสื่อกลาง (ค่าตัวแปร ฯลฯ ) คุณสามารถใช้งานได้eทุกเวลา คุณได้รับพร้อมท์ให้ป้อน sexp เพื่อประเมินผลและพิมพ์ผลการประเมิน

ขณะที่คุณใช้ดีบักเกอร์เก็บสำเนาของซอร์สโค้ดไว้ในเฟรมอื่นเพื่อให้คุณสามารถติดตามสิ่งที่เกิดขึ้น

นอกจากนี้คุณยังสามารถแทรกการโทรอย่างชัดเจนเพื่อเข้าสู่ดีบั๊ก (จุดพักมากหรือน้อยกว่า) ที่ตำแหน่งใดก็ได้ในซอร์สโค้ด คุณสามารถแทรกหรือ(debug) (debug nil SOME-SEXP-TO-EVALUATE)ในกรณีหลังเมื่อSOME-SEXP-TO-EVALUATEมีการประเมินการดีบักและพิมพ์ผลลัพธ์ (โปรดจำไว้ว่าคุณสามารถแทรกรหัสดังกล่าวลงในซอร์สโค้ดและใช้C-M-xในการประเมินได้แล้วเลิกทำ - คุณไม่จำเป็นต้องบันทึกไฟล์ที่แก้ไขแล้ว)

ดูคู่มือ Elisp, โหนดUsing Debuggerสำหรับข้อมูลเพิ่มเติม

การเรียกซ้ำเป็นวนซ้ำ

อย่างไรก็ตามคิดว่าการวนซ้ำเป็นวนซ้ำ มีสองกรณีการเลิกจ้างที่กำหนดไว้คือและ(<= number 0) (= number 1)ในกรณีเหล่านี้ฟังก์ชันจะส่งคืนตัวเลขอย่างง่าย

ในกรณี recursive number - 1ฟังก์ชันจะส่งกลับผลรวมของตัวเลขที่และผลของการทำงานกับที่ ในที่สุดฟังก์ชั่นจะถูกเรียกด้วยอย่างใดอย่างหนึ่ง1หรือจำนวนที่น้อยกว่าหรือเท่ากับศูนย์

ดังนั้นผลลัพธ์ของการเรียกซ้ำจึงเป็นดังนี้:

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

(triangle-using-cond 4)ใช้ตัวอย่างเช่น มารวบรวมการแสดงออกสุดท้าย:

  • ในการทำซ้ำครั้งแรกnumberคือ4ดังนั้น(> number 1)สาขาตาม เราเริ่มต้นสร้างการแสดงออก(+ 4 ...และเรียกฟังก์ชั่นที่มีคือ(1- 4)(triangle-using-cond 3)

  • ตอนนี้numberเป็นและผลที่ได้คือ3 การแสดงออกผลรวมเป็น(+ 3 (triangle-using-cond 2))(+ 4 (+ 3 (triangle-using-cond 2)))

  • numberคือ2ตอนนี้ดังนั้นนิพจน์คือ(+ 4 (+ 3 (+ 2 (triangle-using-cond 1))))

  • numberคือ1ตอนนี้และเราจะใช้สาขาส่งผลให้ในที่น่าเบื่อ(= number 1) การแสดงออกทั้งเป็น1 (+ 4 (+ 3 (+ 2 1)))ประเมินว่าจากภายในออกและคุณจะได้รับ: (+ 4 (+ 3 3)), หรือเพียงแค่(+ 4 6)10


3
Edebug จะดียิ่งขึ้น =)
Malabarba

ทำอย่างไรถึงจะพิมพ์เส้นทางด้วยการmessage (...)กดปุ่มC-x C-eเพียงแค่แสดงผลลัพธ์สุดท้าย (10) ไม่มีอะไรอีก? ฉันพลาดอะไรไปรึเปล่า?
ปริญญาเอก

@Malabarba วิธีEdebugการใช้งาน
ปริญญาเอก

1
@Doctorate ตีC-u C-M-xด้วยจุดภายในฟังก์ชั่นเพื่อ edebug มัน จากนั้นเพียงเรียกใช้ฟังก์ชันตามปกติ
Malabarba

@ กำหนด(message ...)สิ่งที่พิมพ์ไปยัง*Message*บัฟเฟอร์
rekado

6

โมเดลการแทนที่สำหรับแอปพลิเคชันขั้นตอน จาก SICP สามารถอธิบายอัลกอริทึมเพื่อทำความเข้าใจโค้ดเช่นนี้

ฉันเขียนโค้ดเพื่อความสะดวกเช่นนี้ lispy-flattenจากแพ็คเกจlispyทำเช่นนี้ นี่คือผลของการใช้lispy-flattenเพื่อ(triangle-using-cond 4):

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

คุณสามารถทำให้นิพจน์ด้านบนเป็นแบบง่าย ๆ เพียง:

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

จากนั้นแผ่อีกครั้ง:

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

ผลสุดท้าย:

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

3

นี้ไม่ได้เฉพาะเจาะจงกับ Emacs / Elisp แต่ถ้าคุณมีพื้นหลังคณิตศาสตร์แล้วเรียกซ้ำเป็นเหมือนอุปนัยทางคณิตศาสตร์ (หรือถ้าคุณไม่: จากนั้นเมื่อคุณเรียนรู้การเหนี่ยวนำมันก็เหมือนการเรียกซ้ำ!)

เริ่มต้นด้วยคำจำกัดความ:

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

เมื่อnumberเป็น4ทั้งมีเงื่อนไขสองคนแรกถือจึงการประเมินตามสภาพที่สาม
(triangle-using-cond 4)ได้รับการประเมินเป็นคือเป็น
(+ number (triangle-using-cond (1- number)))
(+ 4 (triangle-using-cond 3))

ในทำนองเดียวกันมีการประเมินว่า
(triangle-using-cond 3)
(+ 3 (triangle-using-cond 2))

ในทำนองเดียวกัน มีการประเมินว่า(triangle-using-cond 2)
(+ 2 (triangle-using-cond 1))

แต่สำหรับเงื่อนไขที่สองถือและจะมีการประเมินว่า(triangle-using-cond 1)1

คำแนะนำสำหรับทุกคนที่เรียนรู้การเรียกซ้ำ: พยายามหลีกเลี่ยง

ข้อผิดพลาดเริ่มต้นทั่วไปของการพยายามคิดเกี่ยวกับสิ่งที่เกิดขึ้นในระหว่างการโทรแบบเรียกซ้ำแทนที่จะเชื่อว่าการโทรแบบเรียกซ้ำได้รับผล (บางครั้งเรียกว่าการกระโดดแบบเรียกซ้ำ)

หากคุณพยายามโน้มน้าวใจตัวเองว่า(triangle-using-cond 4)จะคืนคำตอบที่ถูกต้องหรือไม่ให้ลองคิดดูว่า(triangle-using-cond 3)จะส่งคืนคำตอบที่ถูกต้องหรือไม่และตรวจสอบว่าคำตอบนั้นถูกต้องหรือไม่ แน่นอนคุณต้องตรวจสอบกรณีฐานด้วย


2

ขั้นตอนการคำนวณสำหรับตัวอย่างของคุณจะเป็นดังนี้:

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

เงื่อนไข 0 นั้นไม่เคยพบจริงเพราะ 1 เนื่องจากอินพุตสิ้นสุดการสอบถามซ้ำแล้ว


(1)ไม่ใช่นิพจน์ที่ถูกต้อง
rekado

1
M-x calcจะประเมินได้ดีด้วย :-) อย่างจริงจังแม้ว่าฉันตั้งใจจะแสดงการคำนวณไม่ใช่การประเมินเสียงกระเพื่อม
paprika

โอ้ฉันไม่ได้สังเกตว่ามันเป็นคำตอบของคุณ(4 +แทน(+ 4... :)
rekado

0

ฉันคิดว่ามันค่อนข้างง่ายคุณไม่จำเป็นต้อง emacs เสียงกระเพื่อมที่ใต้นี้มันเป็นเพียงคณิตศาสตร์ระดับประถมศึกษา

f (0) = 0

f (1) = 1

f (n) = f (n-1) + n เมื่อ n> 1

ดังนั้น f (5) = 5 + f (4) = 5 + 4 + f (3) = 5 + 4 + 3 + 2 + 1 + 0

ตอนนี้มันชัดเจน


ในกรณีของฟังก์ชั่นนี้จะไม่มีการเรียก f (0)
rekado
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.