นี่เป็นวิธีทั่วไปในการแปลงกระบวนงานแบบเรียกซ้ำใด ๆ ไปเป็นการเรียกซ้ำแบบหางหรือไม่?


13

ดูเหมือนว่าฉันได้พบวิธีทั่วไปในการแปลงขั้นตอนการเรียกซ้ำใด ๆ เป็นการเรียกซ้ำแบบหาง:

  1. กำหนดกระบวนการย่อยผู้ช่วยด้วยพารามิเตอร์ "ผลลัพธ์" พิเศษ
  2. ใช้สิ่งที่จะนำไปใช้กับค่าส่งคืนของโพรซีเดอร์กับพารามิเตอร์นั้น
  3. เรียกขั้นตอนผู้ช่วยนี้เพื่อเริ่มต้น ค่าเริ่มต้นสำหรับพารามิเตอร์ "result" คือค่าสำหรับจุดออกของกระบวนการซ้ำเพื่อให้กระบวนการวนซ้ำผลลัพธ์เริ่มต้นจากจุดที่กระบวนการวนซ้ำเริ่มหดตัว

ตัวอย่างเช่นต่อไปนี้เป็นขั้นตอนการเรียกซ้ำแบบดั้งเดิมที่จะทำการแปลง ( SICP แบบฝึกหัด 1.17 ):

(define (fast-multiply a b)
  (define (double num)
    (* num 2))
  (define (half num)
    (/ num 2))
  (cond ((= b 0) 0)
        ((even? b) (double (fast-multiply a (half b))))
        (else (+ (fast-multiply a (- b 1)) a))))

นี่คือขั้นตอนการแปลงแบบเรียกซ้ำ (tail-recursive) ( SICP แบบฝึกหัด 1.18 ):

(define (fast-multiply a b)
  (define (double n)
    (* n 2))
  (define (half n)
    (/ n 2))
  (define (multi-iter a b product)
    (cond ((= b 0) product)
          ((even? b) (multi-iter a (half b) (double product)))
          (else (multi-iter a (- b 1) (+ product a)))))
  (multi-iter a b 0))

ใครบางคนสามารถพิสูจน์หรือหักล้างสิ่งนี้ได้?


1
O(logn)

ความคิดที่สอง: การเลือกbให้มีกำลัง 2 แสดงว่าการตั้งค่าเริ่มต้นproductเป็น 0 นั้นค่อนข้างไม่ถูกต้อง แต่การเปลี่ยนเป็น 1 จะไม่ทำงานเมื่อbคี่ บางทีคุณอาจต้องการพารามิเตอร์ตัวสะสม 2 ตัวที่แตกต่างกัน?
j_random_hacker

3
คุณยังไม่ได้นิยามการแปลงของนิยามแบบวนซ้ำแบบไม่หางการเพิ่มพารามิเตอร์ผลลัพธ์บางส่วนและการใช้เพื่อการสะสมนั้นค่อนข้างคลุมเครือและแทบจะไม่พูดถึงกรณีที่ซับซ้อนมากขึ้นเช่นทราเวิร์สทรีที่คุณมีการโทรซ้ำสองครั้ง ความคิดที่ชัดเจนยิ่งขึ้นเกี่ยวกับ "ความต่อเนื่อง" มีอยู่ซึ่งคุณทำส่วนหนึ่งของงานแล้วอนุญาตให้ฟังก์ชั่น "ความต่อเนื่อง" รับช่วงต่อโดยรับเป็นพารามิเตอร์สำหรับงานที่คุณได้ทำไปแล้ว มันถูกเรียกว่าต่อเนื่องผ่านสไตล์ (CPS) ดูen.wikipedia.org/wiki/Continuation-passing_style
Ariel

4
สไลด์เหล่านี้fsl.cs.illinois.edu/images/d/d5/CS422-Fall-2006-13.pdfมีคำอธิบายของการแปลง cps ซึ่งคุณใช้นิพจน์โดยพลการ และเปลี่ยนเป็นนิพจน์ที่เทียบเท่าโดยใช้การเรียกหางเท่านั้น
Ariel

@j_random_hacker ใช่ฉันเห็นว่ากระบวนการ "แปลง" ของฉันนั้นผิดจริง ...
nalzok

คำตอบ:


12

รายละเอียดของอัลกอริทึมของคุณนั้นคลุมเครือเกินกว่าที่จะประเมินได้ในตอนนี้ แต่นี่คือสิ่งที่ต้องพิจารณา

CPS

ในความเป็นจริงมีวิธีการแปลงรหัสใด ๆลงในแบบฟอร์มที่ใช้เรียกหางเท่านั้น นี่คือการแปลง CPS CPS ( รูปแบบการส่งผ่านต่อเนื่อง ) เป็นรูปแบบของการแสดงรหัสโดยส่งผ่านแต่ละฟังก์ชั่นต่อเนื่อง ความต่อเนื่องเป็นแนวคิดที่เป็นนามธรรมแทน "ส่วนที่เหลือของการสะสม" ในรหัสที่แสดงในรูปแบบ CPS วิธีธรรมชาติในการจัดลำดับความต่อเนื่องคือฟังก์ชั่นที่รับค่า ใน CPS แทนที่จะเป็นฟังก์ชันที่ส่งคืนค่ามันจะใช้ฟังก์ชันที่แสดงถึงความต่อเนื่องในปัจจุบันกับฟังก์ชันที่ "ส่งคืน" โดยฟังก์ชัน

ตัวอย่างเช่นพิจารณาฟังก์ชันต่อไปนี้:

(lambda (a b c d)
  (+ (- a b) (* c d)))

สิ่งนี้สามารถแสดงใน CPS ดังนี้:

(lambda (k a b c d)
  (- (lambda (v1)
       (* (lambda (v2)
            (+ k v1 v2))
          a b))
     c d))

มันน่าเกลียดและมักจะช้า แต่ก็มีข้อดีบางอย่าง:

  • การแปลงสามารถเป็นไปโดยอัตโนมัติอย่างสมบูรณ์ ดังนั้นไม่จำเป็นต้องเขียน (หรือดู) โค้ดในรูปแบบ CPS
  • เมื่อรวมกับthunking และ trampoliningสามารถใช้เพื่อเพิ่มประสิทธิภาพการโทรแบบหางในภาษาที่ไม่ได้มีการเพิ่มประสิทธิภาพการโทรแบบหาง (การปรับ tail-call ของฟังก์ชั่น tail-recursive โดยตรงสามารถทำได้โดยใช้วิธีอื่นเช่นการแปลงการเรียกแบบเรียกซ้ำไปเป็นลูป แต่การเรียกทางอ้อมนั้นไม่เป็นการแปลงในลักษณะนี้)
  • ด้วย CPS การต่อเนื่องกลายเป็นวัตถุชั้นหนึ่ง เนื่องจากความต่อเนื่องเป็นหัวใจสำคัญของการควบคุมสิ่งนี้ทำให้ผู้ปฏิบัติงานควบคุมแทบทุกคนสามารถนำไปใช้เป็นห้องสมุดโดยไม่ต้องการการสนับสนุนพิเศษจากภาษา ตัวอย่างเช่น goto, ข้อยกเว้นและเธรดสหกรณ์สามารถสร้างแบบจำลองโดยใช้การดำเนินการต่อเนื่อง

TCO

สำหรับฉันแล้วดูเหมือนว่าเหตุผลเดียวที่เกี่ยวข้องกับการเรียกแบบ tail-recursion (หรือการเรียกแบบ tail-tail ทั่วไป) นั้นมีวัตถุประสงค์เพื่อการเพิ่มประสิทธิภาพ tail-call (TCO) ดังนั้นฉันคิดว่าคำถามที่ดีกว่าที่จะถามคือ "รหัสอัตราผลตอบแทนการแปลงของฉันที่เรียกได้ว่าเหมาะสมที่สุดหรือไม่"

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

ก็ไม่มาก คุณเห็นในขณะที่มันอาจปรากฏว่าเราได้กำจัดสแต็คทั้งหมดที่เราทำคือเพียงเปลี่ยนวิธีที่เราเป็นตัวแทน ตอนนี้สแต็กเป็นส่วนหนึ่งของการปิดที่แสดงถึงความต่อเนื่อง ดังนั้น CPS จึงไม่ทำให้การโทรหางของเรามีประสิทธิภาพสูงสุด

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


มันค่อนข้างสับสนเมื่ออยู่ในส่วนย่อย " TCO " เมื่อคุณพูดว่า "tail-call optimization" คุณหมายถึง "การใช้หน่วยความจำคงที่" การใช้งานหน่วยความจำแบบไดนามิกไม่คงที่ยังคงไม่ปฏิเสธความจริงที่ว่าการโทรนั้นเป็นไปตามความต้องการและไม่มีการเติบโตที่ไม่ จำกัด ในการใช้งานสแต็ก SICP เรียกการคำนวณเช่นนี้ว่า "ซ้ำ" ดังนั้นการพูดว่า "แม้ว่าจะเป็น TCO แต่ก็ยังไม่ทำให้ซ้ำได้" อาจเป็นถ้อยคำที่ดีกว่า (สำหรับฉัน)
Will Ness

@ WillNess เรายังมี call stack มันเป็นเพียงการแสดงที่แตกต่างกัน โครงสร้างไม่เปลี่ยนแปลงเพียงเพราะเราใช้ฮีปมากกว่าที่จะเป็นสแต็กของฮาร์ดแวร์ ท้ายที่สุดมีโครงสร้างข้อมูลมากมายตามหน่วยความจำฮีปแบบไดนามิกที่มี "สแต็ค" ในชื่อของพวกเขา
นาธานเดวิส

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