ขณะที่เริ่มต้นที่จะเรียนรู้เสียงกระเพื่อมผมเคยเจอในระยะหาง recursive มันหมายความว่าอะไรกันแน่?
ขณะที่เริ่มต้นที่จะเรียนรู้เสียงกระเพื่อมผมเคยเจอในระยะหาง recursive มันหมายความว่าอะไรกันแน่?
คำตอบ:
พิจารณาฟังก์ชั่นง่าย ๆ ที่เพิ่มตัวเลขธรรมชาติ N ตัวแรก (เช่นsum(5) = 1 + 2 + 3 + 4 + 5 = 15
)
นี่คือการใช้ JavaScript อย่างง่ายที่ใช้การเรียกซ้ำ:
function recsum(x) {
if (x === 1) {
return x;
} else {
return x + recsum(x - 1);
}
}
หากคุณโทรrecsum(5)
มานี่คือสิ่งที่ล่าม JavaScript จะประเมิน:
recsum(5)
5 + recsum(4)
5 + (4 + recsum(3))
5 + (4 + (3 + recsum(2)))
5 + (4 + (3 + (2 + recsum(1))))
5 + (4 + (3 + (2 + 1)))
15
โปรดทราบว่าการเรียกซ้ำแบบเรียกซ้ำจะต้องดำเนินการให้เสร็จสิ้นก่อนที่ล่าม JavaScript จะเริ่มต้นทำงานจริงในการคำนวณผลรวม
นี่คือฟังก์ชั่นรุ่นหลังหางที่เรียกซ้ำได้:
function tailrecsum(x, running_total = 0) {
if (x === 0) {
return running_total;
} else {
return tailrecsum(x - 1, running_total + x);
}
}
นี่คือลำดับเหตุการณ์ที่จะเกิดขึ้นหากคุณโทรหาtailrecsum(5)
(ซึ่งจะเป็นอย่างมีประสิทธิภาพtailrecsum(5, 0)
เนื่องจากอาร์กิวเมนต์ที่สองที่เป็นค่าเริ่มต้น)
tailrecsum(5, 0)
tailrecsum(4, 5)
tailrecsum(3, 9)
tailrecsum(2, 12)
tailrecsum(1, 14)
tailrecsum(0, 15)
15
ในกรณีหางแบบเรียกซ้ำด้วยการประเมินการเรียกซ้ำแบบเรียกซ้ำแต่ละครั้งrunning_total
จะมีการอัพเดต
หมายเหตุ: คำตอบดั้งเดิมใช้ตัวอย่างจาก Python เหล่านี้ได้ถูกเปลี่ยนไปเป็น JavaScript ตั้งแต่ล่ามงูใหญ่ไม่สนับสนุนการเพิ่มประสิทธิภาพการโทรหาง อย่างไรก็ตามในขณะที่การเพิ่มประสิทธิภาพสายหางเป็นส่วนหนึ่งของ ECMAScript 2015 ข้อมูลจำเพาะล่าม JavaScript ส่วนใหญ่ไม่สนับสนุนมัน
tail recursion
จะประสบความสำเร็จในภาษาที่ไม่ได้เพิ่มประสิทธิภาพการโทรหางได้อย่างไร
ในการเรียกใช้แบบเรียกซ้ำรุ่นทั่วไปคือให้คุณทำการเรียกซ้ำซ้ำก่อนแล้วจึงนำค่าที่ส่งคืนของการเรียกซ้ำไปมาและคำนวณผลลัพธ์ ด้วยวิธีนี้คุณจะไม่ได้รับผลการคำนวณจนกว่าจะได้รับการโทรซ้ำทุกครั้ง
ในการเรียกซ้ำแบบหางคุณทำการคำนวณของคุณก่อนแล้วจึงดำเนินการเรียกซ้ำโดยส่งผลลัพธ์ของขั้นตอนปัจจุบันของคุณไปยังขั้นตอนถัดไปที่เกิดซ้ำ (return (recursive-function params))
ผลนี้ในงบที่ผ่านมาอยู่ในรูปแบบของ โดยทั่วไปค่าตอบแทนของขั้นตอน recursive ใดก็ตามเป็นเช่นเดียวกับค่าตอบแทนของโทร
ผลที่ตามมาคือเมื่อคุณพร้อมที่จะทำขั้นตอนการทำซ้ำครั้งต่อไปคุณไม่ต้องการเฟรมสแต็กปัจจุบันอีกต่อไป สิ่งนี้จะช่วยให้การเพิ่มประสิทธิภาพบางอย่าง ในความเป็นจริงมีคอมไพเลอร์เขียนเหมาะสมที่คุณไม่ควรจะมีความแตกล้นขำกับโทร recursive หาง เพียงนำเฟรมสแต็กปัจจุบันมาใช้ซ้ำสำหรับขั้นตอนแบบเรียกซ้ำ ฉันค่อนข้างมั่นใจว่า LISP ทำเช่นนี้
จุดสำคัญคือการเรียกซ้ำหางนั้นเทียบเท่ากับการวนซ้ำ มันไม่ได้เป็นเพียงแค่การเพิ่มประสิทธิภาพของคอมไพเลอร์ แต่เป็นข้อเท็จจริงพื้นฐานเกี่ยวกับการแสดงออก สิ่งนี้ไปได้ทั้งสองวิธี: คุณสามารถวนซ้ำของแบบฟอร์มได้
while(E) { S }; return Q
โดยที่E
และQ
เป็นนิพจน์และS
เป็นลำดับของข้อความสั่งและเปลี่ยนเป็นฟังก์ชันเรียกซ้ำแบบหาง
f() = if E then { S; return f() } else { return Q }
แน่นอนE
, S
และQ
จะต้องมีการกำหนดไว้ในการคำนวณมูลค่าที่น่าสนใจบางตัวแปรบาง ตัวอย่างเช่นฟังก์ชั่นวนรอบ
sum(n) {
int i = 1, k = 0;
while( i <= n ) {
k += i;
++i;
}
return k;
}
เทียบเท่ากับฟังก์ชั่น tail-recursive
sum_aux(n,i,k) {
if( i <= n ) {
return sum_aux(n,i+1,k+i);
} else {
return k;
}
}
sum(n) {
return sum_aux(n,1,0);
}
(นี่คือ "การห่อ" ของฟังก์ชั่น tail-recursive พร้อมฟังก์ชั่นที่มีพารามิเตอร์น้อยลงเป็นสำนวนการทำงานทั่วไป)
else { return k; }
สามารถเปลี่ยนเป็นreturn k;
ข้อความที่ตัดตอนมาจากการเขียนโปรแกรมหนังสือใน Lua นี้แสดงวิธีการเรียกซ้ำหางที่เหมาะสม (ใน Lua แต่ควรนำไปใช้กับ Lisp ด้วย) และทำไมมันถึงดีกว่า
สายหาง [recursion หาง] เป็นชนิดของโกโตะแต่งตัวเป็นโทร การเรียกหางเกิดขึ้นเมื่อฟังก์ชันเรียกอีกอย่างว่าเป็นการกระทำครั้งสุดท้ายดังนั้นจึงไม่มีอะไรให้ทำ ตัวอย่างเช่นในรหัสต่อไปนี้การเรียกเพื่อ
g
เป็นการโทรแบบหาง:function f (x) return g(x) end
หลังจาก
f
โทรg
แล้วไม่มีอะไรทำ ในสถานการณ์เช่นนี้โปรแกรมไม่จำเป็นต้องกลับไปที่ฟังก์ชั่นการโทรเมื่อฟังก์ชั่นที่เรียกว่าจบลง ดังนั้นหลังจากการเรียกแบบหางโปรแกรมไม่จำเป็นต้องเก็บข้อมูลใด ๆ เกี่ยวกับฟังก์ชันการเรียกในสแต็ก ...เนื่องจากการเรียก tail ที่เหมาะสมไม่ใช้พื้นที่สแต็กจึงไม่มีการ จำกัด จำนวนการโทรแบบ "ซ้อน" ที่โปรแกรมสามารถทำได้ ตัวอย่างเช่นเราสามารถเรียกฟังก์ชั่นต่อไปนี้ด้วยหมายเลขใด ๆ เป็นอาร์กิวเมนต์ มันจะไม่ล้นสแต็ค:
function foo (n) if n > 0 then return foo(n - 1) end end
... อย่างที่ฉันพูดไปก่อนหน้านี้การเรียกหางเป็นไปแบบไหน ดังนั้นแอปพลิเคชันที่มีประโยชน์มากสำหรับการโทรหางที่เหมาะสมใน Lua นั้นใช้สำหรับการเขียนโปรแกรมเครื่องจักรของรัฐ แอปพลิเคชันดังกล่าวสามารถเป็นตัวแทนของแต่ละรัฐโดยฟังก์ชั่น; เพื่อเปลี่ยนสถานะคือไปที่ (หรือเพื่อโทร) ฟังก์ชั่นเฉพาะ ตัวอย่างเช่นให้เราพิจารณาเกมเขาวงกตง่ายๆ เขาวงกตมีหลายห้องแต่ละห้องมีประตูสูงสุดถึงสี่ประตู: ทิศเหนือทิศใต้ทิศตะวันออกและทิศตะวันตก ในแต่ละขั้นตอนผู้ใช้จะเข้าสู่ทิศทางการเคลื่อนไหว หากมีประตูไปในทิศทางนั้นผู้ใช้จะไปที่ห้องที่เกี่ยวข้อง มิฉะนั้นโปรแกรมจะพิมพ์คำเตือน เป้าหมายคือไปจากห้องเริ่มต้นไปยังห้องสุดท้าย
เกมนี้เป็นเครื่องรัฐทั่วไปที่ห้องปัจจุบันเป็นของรัฐ เราสามารถใช้เขาวงกตด้วยฟังก์ชั่นเดียวสำหรับแต่ละห้อง เราใช้การโทรหางเพื่อย้ายจากห้องหนึ่งไปอีกห้องหนึ่ง เขาวงกตขนาดเล็กที่มีสี่ห้องอาจมีลักษณะเช่นนี้:
function room1 () local move = io.read() if move == "south" then return room3() elseif move == "east" then return room2() else print("invalid move") return room1() -- stay in the same room end end function room2 () local move = io.read() if move == "south" then return room4() elseif move == "west" then return room1() else print("invalid move") return room2() end end function room3 () local move = io.read() if move == "north" then return room1() elseif move == "east" then return room4() else print("invalid move") return room3() end end function room4 () print("congratulations!") end
ดังนั้นคุณจะเห็นเมื่อคุณโทรซ้ำแบบ:
function x(n)
if n==0 then return 0
n= n-2
return x(n) + 1
end
นี่ไม่ใช่หางแบบเรียกซ้ำเนื่องจากคุณยังมีสิ่งที่ต้องทำ (เพิ่ม 1) ในฟังก์ชันนั้นหลังจากทำการเรียกซ้ำ หากคุณป้อนจำนวนที่สูงมากมันอาจจะทำให้เกิดการล้นสแต็ค
การใช้การเรียกซ้ำแบบปกติการโทรซ้ำแต่ละครั้งจะส่งรายการอื่นไปยังสแต็กการโทร เมื่อการเรียกซ้ำเสร็จสิ้นแล้วแอปจะต้องป๊อปแต่ละรายการออกไปจนสุด
ด้วยการเรียกซ้ำแบบหางขึ้นอยู่กับภาษาคอมไพเลอร์อาจสามารถยุบสแต็กลงเป็นหนึ่งรายการได้ดังนั้นคุณจึงประหยัดพื้นที่สแต็ก ... เคียวรีแบบเรียกซ้ำขนาดใหญ่อาจทำให้กองซ้อนล้นได้จริง
โดยทั่วไปการเรียกซ้ำแบบหางสามารถปรับให้เหมาะกับการวนซ้ำ
ไฟล์ศัพท์แสงมีสิ่งนี้จะพูดเกี่ยวกับความหมายของการเรียกซ้ำหาง:
เรียกซ้ำหาง / n./
หากคุณไม่ได้เบื่อมันให้ดูการเรียกซ้ำหาง
แทนที่จะอธิบายด้วยคำพูดนี่เป็นตัวอย่าง นี่คือ Scheme ของฟังก์ชันแฟกทอเรียล:
(define (factorial x)
(if (= x 0) 1
(* x (factorial (- x 1)))))
นี่คือเวอร์ชั่นของแฟคทอเรียลที่เรียกว่าแบบเรียกซ้ำ:
(define factorial
(letrec ((fact (lambda (x accum)
(if (= x 0) accum
(fact (- x 1) (* accum x))))))
(lambda (x)
(fact x 1))))
คุณจะสังเกตเห็นในเวอร์ชันแรกว่าการเรียกซ้ำแบบเรียกซ้ำเป็นความจริงจะถูกป้อนเข้าสู่นิพจน์การคูณดังนั้นจึงต้องบันทึกสถานะไว้ในสแต็กเมื่อทำการโทรซ้ำ ในเวอร์ชันแบบเรียกซ้ำไม่มีการ S-expression อื่นรอค่าของการเรียกแบบเรียกซ้ำและเนื่องจากไม่มีงานที่ต้องทำเพิ่มเติมสถานะจึงไม่ต้องบันทึกในสแต็ก ตามกฎแล้วฟังก์ชัน Scheme tail-recursive ใช้พื้นที่สแต็กคง
list-reverse
procedure จะทำงานในพื้นที่สแต็คคงที่ แต่จะสร้างและขยายโครงสร้างข้อมูลบน heap ทราเวิร์สทรีสามารถใช้สแต็กจำลองในอาร์กิวเมนต์เพิ่มเติม ฯลฯ
การเรียกซ้ำแบบหางหมายถึงการเรียกแบบเรียกซ้ำเป็นครั้งสุดท้ายในการสอนตรรกะครั้งสุดท้ายในอัลกอริทึมแบบเรียกซ้ำ
โดยทั่วไปในการเรียกซ้ำคุณมีกรณีฐานซึ่งเป็นสิ่งที่หยุดการเรียกซ้ำและเริ่มต้นการโทรกองซ้อน ในการใช้ตัวอย่างคลาสสิกแม้ว่า C-ish มากกว่า Lisp ฟังก์ชันแฟกทอเรียลแสดงการวนรอบแบบวนซ้ำ การเรียกซ้ำเกิดขึ้นหลังจากตรวจสอบเงื่อนไขพื้นฐาน
factorial(x, fac=1) {
if (x == 1)
return fac;
else
return factorial(x-1, x*fac);
}
การเรียกแฟกทอเรียลเริ่มต้นจะเป็นfactorial(n)
ที่fac=1
(ค่าเริ่มต้น) และ n คือหมายเลขที่จะคำนวณแฟคทอเรียล
else
เป็นขั้นตอนที่คุณอาจเรียกว่า "ตัวพิมพ์ใหญ่" แต่ครอบคลุมหลายบรรทัด ฉันเข้าใจคุณผิดหรือสมมติฐานถูกต้องหรือไม่? การเรียกคืนแบบหางทำได้ดีสำหรับเรือเดินสมุทรหนึ่งเส้นเท่านั้น
factorial
ตัวอย่างเป็นเพียงตัวอย่างง่ายๆคลาสสิกที่ทั้งหมด
หมายความว่าแทนที่จะต้องกดตัวชี้คำสั่งบนสแต็คคุณสามารถข้ามไปที่ด้านบนของฟังก์ชันแบบเรียกซ้ำและดำเนินการต่อได้ สิ่งนี้ช่วยให้ฟังก์ชั่นสามารถเรียกคืนได้อย่างไม่มีกำหนดโดยไม่ล้นสแต็ค
ฉันเขียนโพสต์บล็อกในหัวเรื่องซึ่งมีตัวอย่างกราฟิกของลักษณะกรอบสแต็ก
นี่คือข้อมูลโค้ดแบบย่อเปรียบเทียบสองฟังก์ชัน สิ่งแรกคือการเรียกซ้ำแบบดั้งเดิมเพื่อค้นหาแฟคทอเรียลของจำนวนที่กำหนด ครั้งที่สองใช้การเรียกซ้ำหาง
ง่ายมากและเข้าใจง่าย
วิธีง่ายๆในการบอกว่าฟังก์ชั่นวนซ้ำเป็นหางแบบเรียกซ้ำคือถ้ามันคืนค่าที่เป็นรูปธรรมในเคสพื้นฐานหรือไม่ หมายความว่ามันจะไม่คืนค่า 1 หรือจริงหรืออะไรทำนองนั้น มันจะมีความเป็นไปได้ที่จะคืนค่าตัวแปรของวิธีการอย่างใดอย่างหนึ่งมากกว่า
อีกวิธีคือการบอกว่าการเรียกซ้ำนั้นไม่มีการเพิ่มการคำนวณการดัดแปลง ฯลฯ ... ความหมายไม่มีอะไรเลยนอกจากการเรียกซ้ำ
public static int factorial(int mynumber) {
if (mynumber == 1) {
return 1;
} else {
return mynumber * factorial(--mynumber);
}
}
public static int tail_factorial(int mynumber, int sofar) {
if (mynumber == 1) {
return sofar;
} else {
return tail_factorial(--mynumber, sofar * mynumber);
}
}
วิธีที่ดีที่สุดสำหรับฉันที่จะเข้าใจtail call recursion
คือกรณีพิเศษของการเรียกซ้ำที่การโทรครั้งสุดท้าย (หรือการโทรหาง) เป็นฟังก์ชั่นของตัวเอง
เปรียบเทียบตัวอย่างที่มีให้ใน Python:
def recsum(x):
if x == 1:
return x
else:
return x + recsum(x - 1)
^ recursion
def tailrecsum(x, running_total=0):
if x == 0:
return running_total
else:
return tailrecsum(x - 1, running_total + x)
^ RECILION TAIL
ที่คุณสามารถดูในรุ่น recursive x + recsum(x - 1)
ทั่วไปสายสุดท้ายในการป้องกันรหัสคือ ดังนั้นหลังจากการเรียกวิธีการนี้ยังมีการดำเนินการอื่นซึ่งเป็นrecsum
x + ..
อย่างไรก็ตามในเวอร์ชันแบบเรียกซ้ำหางการโทรครั้งสุดท้าย (หรือการโทรแบบหาง) ในบล็อครหัสคือtailrecsum(x - 1, running_total + x)
หมายความว่าการโทรครั้งสุดท้ายจะทำกับวิธีการของตัวเองและไม่มีการดำเนินการใด ๆ หลังจากนั้น
จุดนี้มีความสำคัญเนื่องจากการเรียกซ้ำแบบหางตามที่เห็นในที่นี้ไม่ทำให้หน่วยความจำเพิ่มขึ้นเนื่องจากเมื่อ VM พื้นฐานเห็นฟังก์ชันที่เรียกตัวเองว่าอยู่ในตำแหน่งหาง (นิพจน์สุดท้ายที่ประเมินในฟังก์ชัน) จะกำจัดเฟรมสแต็กปัจจุบัน เรียกว่า Tail Call Optimization (TCO)
NB โปรดจำไว้ว่าตัวอย่างข้างต้นเขียนใน Python ซึ่งรันไทม์ไม่รองรับ TCO นี่เป็นเพียงตัวอย่างเพื่ออธิบายประเด็น TCO รองรับภาษาเช่น Scheme, Haskell และอื่น ๆ
ใน Java ต่อไปนี้เป็นไปได้ของการใช้งานฟังก์ชัน Fibonacci แบบวนซ้ำ:
public int tailRecursive(final int n) {
if (n <= 2)
return 1;
return tailRecursiveAux(n, 1, 1);
}
private int tailRecursiveAux(int n, int iter, int acc) {
if (iter == n)
return acc;
return tailRecursiveAux(n, ++iter, acc + iter);
}
เปรียบเทียบสิ่งนี้กับการใช้แบบเรียกซ้ำมาตรฐาน:
public int recursive(final int n) {
if (n <= 2)
return 1;
return recursive(n - 1) + recursive(n - 2);
}
iter
จะยกเลิกacc
เมื่อiter < (n-1)
ใด
ฉันไม่ใช่โปรแกรมเมอร์ Lisp แต่ฉันคิดว่านี่จะช่วยได้
โดยทั่วไปแล้วมันเป็นรูปแบบของการเขียนโปรแกรมที่การเรียกซ้ำเป็นสิ่งสุดท้ายที่คุณทำ
นี่คือตัวอย่าง Common LISP ที่ใช้แฟคทอเรียลโดยใช้การเรียกซ้ำแบบหาง เนื่องจากลักษณะของกองซ้อนที่น้อยกว่าเราสามารถทำการคำนวณแฟกทอเรียลที่มีขนาดใหญ่อย่างบ้าคลั่ง ...
(defun ! (n &optional (product 1))
(if (zerop n) product
(! (1- n) (* product n))))
และเพื่อความสนุกคุณสามารถลอง (format nil "~R" (! 25))
ในระยะสั้นการเรียกซ้ำแบบหางมีการเรียกซ้ำเป็นคำสั่งสุดท้ายในฟังก์ชั่นเพื่อที่จะได้ไม่ต้องรอการเรียกซ้ำ
ดังนั้นนี่คือการเรียกซ้ำแบบหางเช่น N (x - 1, p * x) เป็นคำสั่งสุดท้ายในฟังก์ชั่นที่คอมไพเลอร์ฉลาดที่จะคิดออกว่ามันสามารถปรับให้เหมาะสำหรับวง (แฟคทอเรียล) p พารามิเตอร์ที่สองดำเนินการมูลค่าผลิตภัณฑ์ระดับกลาง
function N(x, p) {
return x == 1 ? p : N(x - 1, p * x);
}
นี่คือวิธีการเขียนฟังก์ชันแฟกทอเรียลข้างต้นแบบไม่หาง (แม้ว่าคอมไพเลอร์ C ++ บางตัวอาจปรับให้เหมาะสมได้)
function N(x) {
return x == 1 ? 1 : x * N(x - 1);
}
แต่นี่ไม่ใช่:
function F(x) {
if (x == 1) return 0;
if (x == 2) return 1;
return F(x - 1) + F(x - 2);
}
ฉันเขียนโพสต์ยาวที่ชื่อว่า " ทำความเข้าใจการเรียกซ้ำหาง - Visual Studio C ++ - มุมมองการชุมนุม "
นี่คือรุ่น Perl 5 ของtailrecsum
ฟังก์ชั่นที่กล่าวถึงก่อนหน้านี้
sub tail_rec_sum($;$){
my( $x,$running_total ) = (@_,0);
return $running_total unless $x;
@_ = ($x-1,$running_total+$x);
goto &tail_rec_sum; # throw away current stack frame
}
นี่คือข้อความที่ตัดตอนมาจากโครงสร้างและการตีความของโปรแกรมคอมพิวเตอร์เกี่ยวกับการเรียกซ้ำหาง
ในการเปรียบเทียบการวนซ้ำและการเรียกซ้ำเราจะต้องระมัดระวังไม่ให้แนวคิดของกระบวนการเวียนเกิดซ้ำสับสนกับแนวคิดของกระบวนการเรียกซ้ำ เมื่อเราอธิบายขั้นตอนแบบเรียกซ้ำเราจะอ้างถึงข้อเท็จจริงเกี่ยวกับวากยสัมพันธ์ว่านิยามของโพรซีเดอร์อ้างอิง (ไม่ว่าโดยตรงหรือโดยอ้อม) กับกระบวนการนั้น แต่เมื่อเราอธิบายกระบวนการตามรูปแบบที่กล่าวคือเรียกซ้ำแบบเส้นตรงเรากำลังพูดถึงกระบวนการวิวัฒนาการไม่ใช่เกี่ยวกับไวยากรณ์ของวิธีการเขียนขั้นตอน ดูเหมือนว่าอาจเป็นการรบกวนที่เราอ้างถึงกระบวนการเรียกซ้ำเช่นความจริง - iter เป็นการสร้างกระบวนการวนซ้ำ อย่างไรก็ตามกระบวนการจริงๆแล้วซ้ำแล้วซ้ำอีก: สถานะของมันถูกจับอย่างสมบูรณ์โดยสามสถานะของตัวแปรและล่ามต้องติดตามตัวแปรเพียงสามตัวเท่านั้นเพื่อดำเนินการกระบวนการ
เหตุผลหนึ่งที่ทำให้ความแตกต่างระหว่างกระบวนการและขั้นตอนอาจทำให้เกิดความสับสนก็คือการใช้งานส่วนใหญ่ของภาษาทั่วไป (รวมถึง Ada, Pascal และ C) ได้รับการออกแบบในลักษณะที่การตีความของกระบวนการเรียกซ้ำใด ๆ นั้นใช้หน่วยความจำ จำนวนการเรียกใช้โพรซีเดอร์แม้ว่าโดยหลักการแล้วจะเป็นซ้ำก็ตาม ด้วยเหตุนี้ภาษาเหล่านี้สามารถอธิบายกระบวนการวนซ้ำได้โดยการหันไปใช้จุดประสงค์พิเศษ "โครงสร้างวนซ้ำ" เช่นทำซ้ำทำซ้ำจนกระทั่งถึงสำหรับและในขณะที่ การดำเนินการตามโครงการไม่ได้แบ่งปันข้อบกพร่องนี้ มันจะดำเนินการกระบวนการวนซ้ำในพื้นที่คงที่แม้ว่ากระบวนการวนซ้ำจะถูกอธิบายโดยกระบวนงานแบบเรียกซ้ำ การใช้งานกับคุณสมบัตินี้เรียกว่าแบบเรียกซ้ำ ด้วยการใช้หางแบบวนซ้ำ, การวนซ้ำสามารถแสดงได้โดยใช้กลไกการเรียกโพรซีเดอร์ปกติ, เพื่อให้โครงสร้างการทำซ้ำแบบพิเศษนั้นมีประโยชน์ในรูปแบบน้ำตาล syntactic เท่านั้น
ฟังก์ชั่นซ้ำคือฟังก์ชั่นที่เรียกด้วยตัวเอง
มันช่วยให้โปรแกรมเมอร์เขียนโปรแกรมที่มีประสิทธิภาพโดยใช้จำนวนน้อยที่สุดของรหัส
ข้อเสียคือพวกเขาสามารถทำให้เกิดการวนซ้ำไม่สิ้นสุดและผลลัพธ์ที่ไม่คาดคิดอื่น ๆ หากเขียนไม่ถูกต้อง
ฉันจะอธิบายทั้งฟังก์ชั่น Simple Recursive และฟังก์ชั่น Tail Recursive
เพื่อเขียนฟังก์ชันเรียกซ้ำแบบง่าย
จากตัวอย่างที่ระบุ:
public static int fact(int n){
if(n <=1)
return 1;
else
return n * fact(n-1);
}
จากตัวอย่างข้างต้น
if(n <=1)
return 1;
เป็นปัจจัยในการตัดสินใจเมื่อออกจากลูป
else
return n * fact(n-1);
เป็นการประมวลผลจริงที่ต้องทำ
ให้ฉันแบ่งงานทีละคนเพื่อความเข้าใจง่าย
ให้เราดูว่าเกิดอะไรขึ้นภายในถ้าฉันวิ่ง fact(4)
public static int fact(4){
if(4 <=1)
return 1;
else
return 4 * fact(4-1);
}
If
วนรอบล้มเหลวดังนั้นจึงไปelse
วนซ้ำเพื่อให้ส่งคืน4 * fact(3)
ในหน่วยความจำสแต็คเรามี 4 * fact(3)
การแทนที่ n = 3
public static int fact(3){
if(3 <=1)
return 1;
else
return 3 * fact(3-1);
}
If
วนรอบล้มเหลวดังนั้นมันจึงไปelse
วนซ้ำ
ดังนั้นมันจึงกลับมา 3 * fact(2)
จำไว้ว่าเราเรียกว่า `` `4 * ความจริง (3)` '
ผลลัพธ์สำหรับ fact(3) = 3 * fact(2)
จนถึงกองได้ 4 * fact(3) = 4 * 3 * fact(2)
ในหน่วยความจำสแต็คเรามี 4 * 3 * fact(2)
การแทนที่ n = 2
public static int fact(2){
if(2 <=1)
return 1;
else
return 2 * fact(2-1);
}
If
วนรอบล้มเหลวดังนั้นมันจึงไปelse
วนซ้ำ
ดังนั้นมันจึงกลับมา 2 * fact(1)
จำไว้ว่าเราเรียก 4 * 3 * fact(2)
ผลลัพธ์สำหรับ fact(2) = 2 * fact(1)
จนถึงกองได้ 4 * 3 * fact(2) = 4 * 3 * 2 * fact(1)
ในหน่วยความจำสแต็คเรามี 4 * 3 * 2 * fact(1)
การแทนที่ n = 1
public static int fact(1){
if(1 <=1)
return 1;
else
return 1 * fact(1-1);
}
If
ห่วงเป็นจริง
ดังนั้นมันจึงกลับมา 1
จำไว้ว่าเราเรียก 4 * 3 * 2 * fact(1)
ผลลัพธ์สำหรับ fact(1) = 1
จนถึงกองได้ 4 * 3 * 2 * fact(1) = 4 * 3 * 2 * 1
ในที่สุดผลลัพธ์ของข้อเท็จจริง (4) = 4 * 3 * 2 * 1 = 24
การเรียกซ้ำแบบหางจะเป็น
public static int fact(x, running_total=1) {
if (x==1) {
return running_total;
} else {
return fact(x-1, running_total*x);
}
}
public static int fact(4, running_total=1) {
if (x==1) {
return running_total;
} else {
return fact(4-1, running_total*4);
}
}
If
วนรอบล้มเหลวดังนั้นจึงไปelse
วนซ้ำเพื่อให้ส่งคืนfact(3, 4)
ในหน่วยความจำสแต็คเรามี fact(3, 4)
การแทนที่ n = 3
public static int fact(3, running_total=4) {
if (x==1) {
return running_total;
} else {
return fact(3-1, 4*3);
}
}
If
วนรอบล้มเหลวดังนั้นมันจึงไปelse
วนซ้ำ
ดังนั้นมันจึงกลับมา fact(2, 12)
ในหน่วยความจำสแต็คเรามี fact(2, 12)
การแทนที่ n = 2
public static int fact(2, running_total=12) {
if (x==1) {
return running_total;
} else {
return fact(2-1, 12*2);
}
}
If
วนรอบล้มเหลวดังนั้นมันจึงไปelse
วนซ้ำ
ดังนั้นมันจึงกลับมา fact(1, 24)
ในหน่วยความจำสแต็คเรามี fact(1, 24)
การแทนที่ n = 1
public static int fact(1, running_total=24) {
if (x==1) {
return running_total;
} else {
return fact(1-1, 24*1);
}
}
If
ห่วงเป็นจริง
ดังนั้นมันจึงกลับมา running_total
ผลลัพธ์สำหรับ running_total = 24
ในที่สุดผลลัพธ์ของข้อเท็จจริง (4,1) = 24
การเรียกคืนแบบหางคือชีวิตที่คุณอาศัยอยู่ในตอนนี้ คุณรีไซเคิลเฟรมสแต็คเดียวกันซ้ำ ๆ ซ้ำ ๆ ตลอดเวลาเนื่องจากไม่มีเหตุผลหรือวิธีการกลับไปที่เฟรม "ก่อนหน้า" อดีตผ่านไปแล้วและทำไปแล้วเพื่อให้สามารถละทิ้งได้ คุณจะได้รับหนึ่งเฟรมตลอดไปสู่อนาคตจนกว่ากระบวนการของคุณจะตายอย่างหลีกเลี่ยงไม่ได้
การเปรียบเทียบแบ่งลงเมื่อคุณพิจารณากระบวนการบางอย่างอาจใช้เฟรมเพิ่มเติม แต่ก็ยังถือว่าเป็นแบบเรียกซ้ำถ้าสแต็กไม่โตขึ้นเรื่อย ๆ
tail recursion เป็นฟังก์ชันแบบเรียกซ้ำโดยที่ฟังก์ชันเรียกตัวเองที่ท้าย ("tail") ของฟังก์ชันที่ไม่มีการคำนวณใด ๆ หลังจากการเรียก recursive กลับมา คอมไพเลอร์จำนวนมากปรับให้เหมาะสมเพื่อเปลี่ยนการเรียกแบบเรียกซ้ำเป็นหางแบบเรียกซ้ำหรือการเรียกซ้ำ
พิจารณาปัญหาของการคำนวณแฟคทอเรียลของตัวเลข
แนวทางที่ตรงไปตรงมาคือ:
factorial(n):
if n==0 then 1
else n*factorial(n-1)
สมมติว่าคุณเรียกแฟคทอเรียล (4) ต้นไม้เรียกซ้ำจะเป็น:
factorial(4)
/ \
4 factorial(3)
/ \
3 factorial(2)
/ \
2 factorial(1)
/ \
1 factorial(0)
\
1
ความลึกสูงสุดของการเรียกซ้ำในกรณีข้างต้นคือ O (n)
อย่างไรก็ตามพิจารณาตัวอย่างต่อไปนี้:
factAux(m,n):
if n==0 then m;
else factAux(m*n,n-1);
factTail(n):
return factAux(1,n);
ต้นไม้เรียกซ้ำสำหรับ factTail (4) จะเป็น:
factTail(4)
|
factAux(1,4)
|
factAux(4,3)
|
factAux(12,2)
|
factAux(24,1)
|
factAux(24,0)
|
24
ความลึกการเรียกซ้ำสูงสุดคือ O (n) แต่ไม่มีการเรียกใดที่เพิ่มตัวแปรพิเศษใด ๆ ลงในสแต็ก ดังนั้นคอมไพเลอร์สามารถทำไปกับกอง
Tail Recursion ค่อนข้างเร็วเมื่อเทียบกับการเรียกซ้ำแบบปกติ มันเร็วเพราะเอาท์พุทของการเรียกบรรพบุรุษจะไม่ถูกเขียนในกองเพื่อติดตาม แต่ในการเรียกซ้ำปกติบรรพบุรุษทั้งหมดเรียกเอาท์พุทที่เขียนไว้ในกองเพื่อติดตาม
หาง recursiveฟังก์ชันเป็นฟังก์ชันเวียนที่การดำเนินการที่ผ่านมามันไม่ก่อนที่จะกลับมีที่ทำให้การเรียกใช้ฟังก์ชัน recursive นั่นคือค่าส่งคืนของการเรียกฟังก์ชันแบบเรียกซ้ำจะถูกส่งคืนทันที ตัวอย่างเช่นรหัสของคุณจะเป็นดังนี้:
def recursiveFunction(some_params):
# some code here
return recursiveFunction(some_args)
# no code after the return statement
คอมไพเลอร์และล่ามที่ใช้การปรับให้เหมาะสมของtail callหรือtail call eliminationสามารถปรับโค้ด recursive ให้เหมาะสมเพื่อป้องกัน stack overflows หากคอมไพเลอร์หรือล่ามของคุณไม่ได้ใช้การเพิ่มประสิทธิภาพการโทรหาง (เช่นล่าม CPython) จะไม่มีประโยชน์เพิ่มเติมในการเขียนรหัสของคุณด้วยวิธีนี้
ตัวอย่างเช่นนี่เป็นฟังก์ชันแฟกทอเรียลแบบเรียกซ้ำใน Python:
def factorial(number):
if number == 1:
# BASE CASE
return 1
else:
# RECURSIVE CASE
# Note that `number *` happens *after* the recursive call.
# This means that this is *not* tail call recursion.
return number * factorial(number - 1)
และนี่คือฟังก์ชั่นแฟกทอเรียลแบบเรียกซ้ำ (tail call):
def factorial(number, accumulator=1):
if number == 0:
# BASE CASE
return accumulator
else:
# RECURSIVE CASE
# There's no code after the recursive call.
# This is tail call recursion:
return factorial(number - 1, number * accumulator)
print(factorial(5))
(โปรดทราบว่าแม้ว่านี่จะเป็นรหัส Python ล่าม CPython จะไม่ทำการเพิ่มประสิทธิภาพการโทรหางดังนั้นการจัดเรียงรหัสของคุณในลักษณะนี้จะช่วยให้ไม่เกิดประโยชน์ขณะใช้งาน)
คุณอาจต้องทำให้โค้ดของคุณอ่านไม่ได้อีกเล็กน้อยเพื่อใช้ประโยชน์จาก tail call อย่างที่แสดงในตัวอย่างปัจจัย (ตัวอย่างเช่นกรณีฐานตอนนี้ใช้งานง่ายเล็กน้อยและaccumulator
พารามิเตอร์ถูกใช้อย่างมีประสิทธิภาพเป็นตัวแปรทั่วโลก)
แต่ข้อดีของการเพิ่มประสิทธิภาพการโทรหางคือมันป้องกันข้อผิดพลาดล้นสแต็ค (ฉันจะทราบว่าคุณสามารถได้รับผลประโยชน์แบบเดียวกันนี้โดยใช้อัลกอริทึมซ้ำ ๆ แทนแบบเรียกซ้ำ)
Stack overflows เกิดขึ้นเมื่อ call stack มีวัตถุเฟรมมากเกินไป วัตถุเฟรมจะถูกผลักลงในสแต็คการโทรเมื่อมีการเรียกใช้ฟังก์ชันและแตกออกจากสแต็กการโทรเมื่อฟังก์ชันส่งคืน วัตถุเฟรมมีข้อมูลเช่นตัวแปรท้องถิ่นและบรรทัดของรหัสที่จะกลับไปเมื่อฟังก์ชั่นกลับมา
หากฟังก์ชันเรียกซ้ำของคุณทำการโทรซ้ำซ้ำมากเกินไปโดยไม่ส่งคืนสแต็กการเรียกอาจเกินขีด จำกัด วัตถุเฟรม (จำนวนแตกต่างกันไปตามแพลตฟอร์มใน Python เป็นวัตถุเฟรม 1000 โดยค่าเริ่มต้น) ซึ่งทำให้เกิดข้อผิดพลาดสแตกล้น (เฮ้นั่นคือที่มาของชื่อเว็บไซต์นี้!)
อย่างไรก็ตามหากสิ่งสุดท้ายที่ฟังก์ชั่น recursive ของคุณทำคือการเรียกซ้ำและส่งกลับค่าส่งคืนแล้วไม่มีเหตุผลที่จะต้องเก็บวัตถุเฟรมปัจจุบันไว้ให้อยู่ใน call stack ท้ายที่สุดถ้าไม่มีรหัสหลังจากการเรียกฟังก์ชันแบบเรียกซ้ำไม่มีเหตุผลที่จะพิจารณาตัวแปรท้องถิ่นของวัตถุเฟรมปัจจุบัน ดังนั้นเราสามารถกำจัดวัตถุเฟรมปัจจุบันได้ทันทีแทนที่จะเก็บไว้ใน call stack ผลลัพธ์สุดท้ายของสิ่งนี้คือสแต็คการโทรของคุณมีขนาดไม่โตจึงไม่สามารถสแต็กล้นได้
คอมไพเลอร์หรือล่ามต้องมีการปรับแต่ง tail call ให้เป็นคุณสมบัติเพื่อให้สามารถจดจำได้เมื่อสามารถใช้การปรับ tail tail ได้อย่างเหมาะสม ถึงแม้ว่าคุณอาจจะจัดเรียงโค้ดในฟังก์ชั่นวนซ้ำเพื่อใช้ประโยชน์จากการเพิ่มประสิทธิภาพการโทรหางและมันก็ขึ้นอยู่กับคุณว่าการลดลงของความสามารถในการอ่านนี้จะคุ้มค่ากับการเพิ่มประสิทธิภาพหรือไม่
เพื่อให้เข้าใจถึงความแตกต่างที่สำคัญระหว่างการเรียกแบบ tail-call และการโทรแบบ tail-call-recursion เราสามารถสำรวจการใช้งาน NET ของเทคนิคเหล่านี้
นี่คือบทความที่มีตัวอย่างบางส่วนใน C #, F # และ C ++ \ CLI: การผจญภัยใน Recursion หางใน C #, F # และ C
C # ไม่ปรับการเรียกซ้ำแบบ tail-call ในขณะที่ F # ทำ
ความแตกต่างของหลักการเกี่ยวข้องกับลูปกับแคลคูลัสแลมบ์ดา C # ได้รับการออกแบบโดยคำนึงถึงลูปในขณะที่ F # สร้างขึ้นจากหลักการของแคลคูลัสแลมบ์ดา สำหรับการที่ดีมาก (และฟรี) หนังสือเกี่ยวกับหลักการของแลมบ์ดาแคลคูลัสดูโครงสร้างและการแปลความหมายของโปรแกรมคอมพิวเตอร์โดย Abelson, Sussman และ Sussman
เกี่ยวกับการโทรหาง F # สำหรับบทความแนะนำที่ดีมากดูรายละเอียดเบื้องต้นเกี่ยวกับการโทรหางใน F # สุดท้ายนี่เป็นบทความที่ครอบคลุมความแตกต่างระหว่างการเรียกซ้ำไม่ใช่หาง recursion หางโทร (ใน F #): หาง recursion เทียบกับที่ไม่ใช่หาง recursion ใน F ที่คมชัด
หากคุณต้องการที่จะอ่านเกี่ยวกับบางส่วนของความแตกต่างของการออกแบบ recursion หางโทรระหว่าง C # และ F # ดูฝ่ายผลิต Tail-Call Opcode ใน C # และ F #
หากคุณสนใจพอที่จะต้องการที่จะรู้ว่าสิ่งที่ป้องกันไม่ให้เงื่อนไขคอมไพเลอร์ C # จากการปฏิบัติเพิ่มประสิทธิภาพหางเรียกดูบทความนี้: JIT CLR เงื่อนไขหางโทร
การเรียกซ้ำมีสองชนิดพื้นฐาน: การเรียกซ้ำแบบหัวและแบบวนซ้ำ
ในการเรียกใช้เฮดเดอร์ฟังก์ชันจะทำการเรียกซ้ำและทำการคำนวณเพิ่มเติมบางครั้งอาจใช้ผลลัพธ์ของการโทรซ้ำ
ในฟังก์ชั่นแบบเรียกซ้ำการคำนวณทั้งหมดเกิดขึ้นก่อนและการเรียกซ้ำเป็นสิ่งสุดท้ายที่เกิดขึ้น
ที่นำมาจากนี้โพสต์ที่น่ากลัวสุด โปรดพิจารณาการอ่าน
การเรียกซ้ำหมายถึงฟังก์ชันที่เรียกตัวเอง ตัวอย่างเช่น:
(define (un-ended name)
(un-ended 'me)
(print "How can I get here?"))
Tail-Recursion หมายถึงการเรียกซ้ำที่สรุปฟังก์ชัน:
(define (un-ended name)
(print "hello")
(un-ended 'me))
ดูสิ่งสุดท้ายฟังก์ชั่นไม่สิ้นสุด (ขั้นตอนในศัพท์แสง Scheme) ทำคือการเรียกตัวเอง อีกตัวอย่าง (มีประโยชน์มากกว่า) คือ:
(define (map lst op)
(define (helper done left)
(if (nil? left)
done
(helper (cons (op (car left))
done)
(cdr left))))
(reverse (helper '() lst)))
ในขั้นตอนผู้ช่วยสิ่งสุดท้ายที่ทำถ้าด้านซ้ายไม่ใช่ศูนย์คือการเรียกตัวเอง (หลังจากข้อเสียบางอย่างและบางสิ่งบางอย่าง cdr) นี่คือวิธีที่คุณแมปรายการ
การเรียกซ้ำแบบหางมีข้อดีอย่างมากที่ล่าม (หรือคอมไพเลอร์ขึ้นอยู่กับภาษาและผู้ขาย) สามารถปรับให้เหมาะสมและแปลงให้เป็นสิ่งที่เทียบเท่ากับลูปสักครู่ ตามความเป็นจริงตามธรรมเนียม Scheme ส่วนใหญ่ "สำหรับ" และ "ในขณะที่" วนรอบจะทำในลักษณะวนรอบแบบเรียกซ้ำ (ไม่มีในขณะที่เท่าที่ฉันรู้)
คำถามนี้มีคำตอบที่ดีมาก ... แต่ฉันไม่สามารถช่วย แต่พูดสอดกับทางเลือกในการกำหนด "tail recursion" หรืออย่างน้อยก็เป็น "tail recursion ที่เหมาะสม" คือ: เราควรมองว่ามันเป็นคุณสมบัติของนิพจน์เฉพาะในโปรแกรมหรือไม่? หรือเราควรมองว่ามันเป็นคุณสมบัติของการใช้ภาษาโปรแกรม ?
สำหรับข้อมูลเพิ่มเติมในมุมมองหลังมีบทความคลาสสิกโดย Will Clinger "การเรียกคืนแบบหางที่เหมาะสมและการใช้พื้นที่อย่างมีประสิทธิภาพ" (PLDI 1998) ที่กำหนด "การเรียกซ้ำแบบหางที่เหมาะสม" เป็นคุณสมบัติของการใช้ภาษาโปรแกรม คำจำกัดความถูกสร้างขึ้นเพื่ออนุญาตให้คนหนึ่งละเว้นรายละเอียดการใช้งาน (เช่นแสดงว่าสแตกการโทรจริงแสดงผ่านทางรันไทม์สแต็กหรือผ่านทางรายการที่เชื่อมโยงของเฟรมที่จัดสรรฮีป)
เพื่อให้บรรลุนี้มันใช้การวิเคราะห์เชิง: ไม่ได้ของเวลาการทำงานของโปรแกรมเป็นหนึ่งมักจะเห็น แต่ของโปรแกรมการใช้พื้นที่ ด้วยวิธีนี้การใช้พื้นที่ของรายการที่เชื่อมโยงที่จัดสรรฮีปเทียบกับสแต็กการเรียกใช้รันไทม์สิ้นสุดลงเทียบเท่ากับ asymptotically ดังนั้นเราจึงไม่สนใจว่ารายละเอียดการใช้ภาษาโปรแกรม (รายละเอียดที่แน่นอนค่อนข้างน้อยในทางปฏิบัติ แต่สามารถโคลนน้ำค่อนข้างน้อยเมื่อหนึ่งพยายามที่จะตรวจสอบว่าการดำเนินการที่กำหนดเป็นที่น่าพอใจความต้องการที่จะ "คุณสมบัติหาง recursive" )
กระดาษมีค่าการศึกษาอย่างรอบคอบด้วยเหตุผลหลายประการ:
มันให้คำนิยามอุปนัยของการแสดงออกหางและเรียกหางของโปรแกรม (คำจำกัดความดังกล่าวและสาเหตุที่การโทรดังกล่าวมีความสำคัญน่าจะเป็นหัวข้อของคำตอบอื่น ๆ ที่ให้ไว้ที่นี่)
นี่คือคำจำกัดความเหล่านี้เพียงเพื่อให้ได้รสชาติของข้อความ:
นิยาม 1 แสดงออกหางของโปรแกรมที่เขียนในหลักโครงการจะมีการกำหนด inductively ดังต่อไปนี้
- ร่างกายของการแสดงออกแลมบ์ดาคือการแสดงออกหาง
- หาก
(if E0 E1 E2)
เป็นการแสดงออกหางแล้วทั้งสองE1
และE2
มีการแสดงออกหาง- ไม่มีอะไรอื่นที่แสดงออกหาง
คำจำกัดความ 2การเรียกแบบหางเป็นการแสดงออกแบบหางที่เป็นการเรียกขั้นตอน
(การเรียกแบบเรียกซ้ำแบบหางหรือตามที่กระดาษบอกว่า "การโทรแบบหางตัวเอง" เป็นกรณีพิเศษของการโทรแบบหางที่กระบวนการถูกเรียกใช้เอง)
มันให้คำจำกัดความอย่างเป็นทางการสำหรับ "เครื่องจักร" หกชนิดที่แตกต่างกันสำหรับการประเมิน Core Scheme โดยที่แต่ละเครื่องมีพฤติกรรมที่สังเกตได้เหมือนกันยกเว้นคลาสที่ซับซ้อนเชิงพื้นที่ที่ไม่มีสัญลักษณ์
ตัวอย่างเช่นหลังจากให้คำจำกัดความสำหรับเครื่องที่มีลำดับ 1. การจัดการหน่วยความจำแบบกองซ้อน 2. การรวบรวมขยะ แต่ไม่มีการเรียกแบบหาง 3. การรวบรวมขยะและการเรียกแบบหางกระดาษจะยังคงดำเนินต่อไปด้วยกลยุทธ์การจัดการพื้นที่เก็บข้อมูลขั้นสูงยิ่งขึ้นเช่น 4. "evlis tail recursion" ซึ่งไม่จำเป็นต้องรักษาสภาพแวดล้อมไว้ตลอดการประเมินผลการโต้แย้งย่อยนิพจน์สุดท้ายในการเรียกหาง 5. ลดสภาพแวดล้อมของการปิดเป็นเพียงตัวแปรอิสระของการปิดนั้นและ 6. ความหมายที่เรียกว่า "พื้นที่ปลอดภัย" ตามที่Appel และ Shaoกำหนด
เพื่อที่จะพิสูจน์ว่าเครื่องจักรนั้นอยู่ในหกชั้นของความซับซ้อนของพื้นที่ที่แตกต่างกันกระดาษสำหรับแต่ละคู่ของเครื่องจักรภายใต้การเปรียบเทียบให้ตัวอย่างที่เป็นรูปธรรมของโปรแกรมที่จะแสดงการระเบิดพื้นที่ asymptotic บนเครื่องหนึ่ง แต่ไม่ใช่อีกเครื่อง
(อ่านคำตอบของฉันตอนนี้ฉันไม่แน่ใจว่าฉันจัดการเพื่อจับจุดสำคัญของกระดาษ Clingerจริง ๆ แต่อนิจจาฉันไม่สามารถอุทิศเวลามากขึ้นในการพัฒนาคำตอบในตอนนี้)
หลายคนอธิบายการเรียกซ้ำแล้วซ้ำอีกที่นี่ ฉันอยากจะพูดถึงความคิดสองสามข้อเกี่ยวกับข้อดีบางประการที่การเรียกซ้ำได้รับจากหนังสือ“ Concurrency in .NET, รูปแบบทันสมัยของการเขียนโปรแกรมพร้อมกันและขนาน” โดย Riccardo Terrell:
“ การเรียกใช้ฟังก์ชั่นซ้ำเป็นวิธีธรรมชาติในการทำซ้ำใน FP เพราะหลีกเลี่ยงการกลายพันธุ์ของรัฐ ในระหว่างการทำซ้ำแต่ละครั้งจะมีการส่งค่าใหม่ไปยังตัวสร้างการวนซ้ำแทนเพื่ออัปเดต (กลายพันธุ์) นอกจากนี้ยังสามารถสร้างฟังก์ชั่นวนซ้ำได้ซึ่งจะทำให้โปรแกรมของคุณเป็นแบบแยกส่วนมากขึ้นรวมถึงแนะนำโอกาสในการใช้ประโยชน์จากการขนานกัน "
นี่คือบันทึกย่อที่น่าสนใจจากหนังสือเล่มเดียวกันเกี่ยวกับการเรียกซ้ำหาง:
Tail-call recursion เป็นเทคนิคที่แปลงฟังก์ชั่น recursive ปกติให้เป็นเวอร์ชั่นที่ปรับให้เหมาะสมที่สุดที่สามารถจัดการอินพุตขนาดใหญ่ได้โดยไม่มีความเสี่ยงและผลข้างเคียง
หมายเหตุเหตุผลหลักสำหรับการโทรแบบหางเป็นการปรับให้เหมาะสมคือการปรับปรุงตำแหน่งข้อมูลการใช้หน่วยความจำและการใช้แคช โดยการโทรหางผู้ใช้ใช้พื้นที่สแต็คเดียวกันกับผู้โทร สิ่งนี้จะช่วยลดความดันหน่วยความจำ มันปรับปรุงแคชเล็กน้อยเนื่องจากมีการใช้หน่วยความจำเดียวกันสำหรับผู้โทรที่ตามมาและสามารถอยู่ในแคชได้แทนที่จะวนบรรทัดแคชที่เก่ากว่าเพื่อให้มีที่ว่างสำหรับบรรทัดแคชใหม่