เหตุใดฟังก์ชันใน Ocaml / F # จึงไม่เรียกซ้ำตามค่าเริ่มต้น


105

เหตุใดฟังก์ชันใน F # และ Ocaml (และอาจเป็นภาษาอื่น ๆ ) จึงไม่เรียกใช้ซ้ำตามค่าเริ่มต้น

กล่าวอีกนัยหนึ่งว่าเหตุใดนักออกแบบภาษาจึงตัดสินใจว่าควรกำหนดให้คุณพิมพ์recคำประกาศอย่างชัดเจนเช่น:

let rec foo ... = ...

และไม่ให้ฟังก์ชันเรียกซ้ำตามค่าเริ่มต้น? ทำไมต้องมีrecโครงสร้างที่ชัดเจน?


คำตอบ:


88

ลูกหลานชาวฝรั่งเศสและอังกฤษของ ML ดั้งเดิมได้เลือกทางเลือกที่แตกต่างกันและทางเลือกของพวกเขาได้รับการสืบทอดมาหลายทศวรรษจนถึงรูปแบบที่ทันสมัย ดังนั้นนี่เป็นเพียงมรดก แต่ส่งผลต่อสำนวนในภาษาเหล่านี้

ฟังก์ชันจะไม่เรียกซ้ำตามค่าเริ่มต้นในกลุ่มภาษาฝรั่งเศส CAML (รวมถึง OCaml) ตัวเลือกนี้ทำให้ง่ายต่อการใช้คำจำกัดความของฟังก์ชัน (และตัวแปร) โดยใช้letในภาษาเหล่านั้นมากเนื่องจากคุณสามารถอ้างถึงคำจำกัดความก่อนหน้าภายในเนื้อหาของคำจำกัดความใหม่ F # สืบทอดไวยากรณ์นี้จาก OCaml

ตัวอย่างเช่นการใช้ฟังก์ชัน superceding pเมื่อคำนวณเอนโทรปีของ Shannon ของลำดับใน OCaml:

let shannon fold p =
  let p x = p x *. log(p x) /. log 2.0 in
  let p t x = t +. p x in
  -. fold p 0.0

สังเกตว่าอาร์กิวเมนต์pของshannonฟังก์ชันลำดับที่สูงกว่าจะถูกแทนที่ด้วยอีกรายการหนึ่งpในบรรทัดแรกของเนื้อหาอย่างไรpในบรรทัดที่สองของเนื้อหาอย่างไร

ในทางกลับกันสาขา SML ของอังกฤษของกลุ่มภาษา ML ได้ใช้ตัวเลือกอื่นและfunฟังก์ชัน -bound ของ SML จะเรียกซ้ำตามค่าเริ่มต้น เมื่อนิยามฟังก์ชันส่วนใหญ่ไม่จำเป็นต้องเข้าถึงการเชื่อมโยงก่อนหน้าของชื่อฟังก์ชันผลลัพธ์นี้จะทำให้โค้ดง่ายขึ้น แต่ฟังก์ชั่นแทนที่จะทำเพื่อให้ใช้ชื่อที่แตกต่างกัน ( f1, f2ฯลฯ ) ซึ่งเป็นมลพิษขอบเขตและทำให้มันเป็นไปได้ที่จะเผลอเรียกผิด "รุ่น" ของฟังก์ชั่น และตอนนี้จะมีความแตกต่างระหว่างปริยาย-recursive funฟังก์ชั่น -bound และไม่ใช่ recursive valฟังก์ชั่น -bound

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

สังเกตว่าคำตอบที่พระพิฆเนศและเอ็ดดี้ให้ไว้คือปลาชนิดหนึ่งสีแดง พวกเขาอธิบายว่าเหตุใดกลุ่มของฟังก์ชันจึงไม่สามารถวางไว้ในยักษ์ได้let rec ... and ...เนื่องจากมีผลต่อเมื่อตัวแปรประเภทได้รับการทำให้เป็นแบบทั่วไป สิ่งนี้ไม่เกี่ยวข้องกับrecการเป็นค่าเริ่มต้นใน SML แต่ไม่ใช่ OCaml


3
ฉันไม่คิดว่าพวกมันเป็นปลาเฮอริ่งแดง: ถ้าไม่ใช่เพราะข้อ จำกัด ในการอนุมานก็เป็นไปได้ว่าโปรแกรมหรือโมดูลทั้งหมดจะได้รับการปฏิบัติโดยอัตโนมัติเหมือนการเรียกซ้ำร่วมกันเหมือนภาษาอื่น ๆ ส่วนใหญ่ นั่นจะทำให้การตัดสินใจออกแบบเฉพาะว่าควรจะต้องมี "rec" หรือไม่
GS - ขอโทษโมนิกา

"... ถือว่าเป็นการเรียกซ้ำร่วมกันโดยอัตโนมัติเหมือนกับภาษาอื่น ๆ ส่วนใหญ่" BASIC, C, C ++, Clojure, Erlang, F #, Factor, Forth, Fortran, Groovy, OCaml, Pascal, Smalltalk และ Standard ML ไม่ได้
JD

3
C / C ++ ต้องการเฉพาะต้นแบบสำหรับคำจำกัดความข้างหน้าซึ่งไม่ได้เกี่ยวกับการทำเครื่องหมายการเรียกซ้ำอย่างชัดเจน Java, C # และ Perl มีการเรียกซ้ำโดยปริยายอย่างแน่นอน เราอาจถกเถียงกันไม่รู้จบเกี่ยวกับความหมายของ "ส่วนใหญ่" และความสำคัญของแต่ละภาษาดังนั้นเรามาดูภาษาอื่น ๆ "มาก ๆ " กันดีกว่า
GS - ขอโทษโมนิกา

3
"C / C ++ ต้องการเฉพาะต้นแบบสำหรับคำจำกัดความข้างหน้าซึ่งไม่ได้เกี่ยวกับการทำเครื่องหมายการเรียกซ้ำอย่างชัดเจน" เฉพาะในกรณีพิเศษของการเรียกซ้ำด้วยตนเอง ในกรณีทั่วไปของการเรียกซ้ำร่วมกันการประกาศไปข้างหน้ามีผลบังคับใช้ทั้งใน C และ C ++
JD

2
การประกาศไปข้างหน้าไม่จำเป็นต้องใช้ใน C ++ ในคลาสสโคปกล่าวคือเมธอดแบบคงที่สามารถเรียกกันได้โดยไม่ต้องประกาศใด ๆ
polkovnikov.ph

52

เหตุผลสำคัญประการหนึ่งสำหรับการใช้ไฟล์ recคือการใช้การอนุมานประเภท Hindley-Milner ซึ่งอยู่ภายใต้ภาษาการเขียนโปรแกรมเชิงฟังก์ชันที่พิมพ์แบบคงที่ทั้งหมด (แม้ว่าจะมีการเปลี่ยนแปลงและขยายในรูปแบบต่างๆ)

หากคุณมีคำจำกัดความlet f x = xคุณคาดว่าจะมีประเภท'a -> 'aและสามารถใช้ได้กับ'aประเภทต่างๆในจุดต่างๆ แต่ถ้าคุณเขียนlet g x = (x + 1) + ...คุณคาดหวังว่าxจะได้รับการปฏิบัติเหมือนเป็นintส่วนที่เหลือของเนื้อหาgในส่วนที่เหลือของร่างกายของ

วิธีที่การอนุมานของ Hindley-Milner เกี่ยวข้องกับความแตกต่างนี้คือผ่านขั้นตอนการวางนัยทั่วไปอย่างชัดเจน ในบางจุดเมื่อประมวลผลโปรแกรมของคุณระบบประเภทจะหยุดและพูดว่า "ตกลงประเภทของคำจำกัดความเหล่านี้จะได้รับการสรุปโดยทั่วไป ณ จุดนี้ดังนั้นเมื่อมีคนใช้ตัวแปรประเภทฟรีใด ๆ ในประเภทของพวกเขาจะสดใหม่สร้างอินสแตนซ์ดังนั้น จะไม่รบกวนการใช้งานอื่น ๆ ของคำจำกัดความนี้ "

ปรากฎว่าสถานที่ที่เหมาะสมในการทำลักษณะทั่วไปนี้คือหลังจากตรวจสอบชุดฟังก์ชันที่เรียกซ้ำซึ่งกันและกัน ก่อนหน้านี้และคุณจะสรุปมากเกินไปจนนำไปสู่สถานการณ์ที่ประเภทต่างๆอาจชนกัน ในภายหลังและคุณจะสรุปน้อยเกินไปทำให้คำจำกัดความที่ไม่สามารถใช้กับอินสแตนซ์ประเภทต่างๆได้

ดังนั้นเนื่องจากตัวตรวจสอบประเภทจำเป็นต้องทราบเกี่ยวกับชุดคำจำกัดความที่เรียกซ้ำร่วมกันมันสามารถทำอะไรได้บ้าง? ความเป็นไปได้อย่างหนึ่งคือทำการวิเคราะห์การพึ่งพาคำจำกัดความทั้งหมดในขอบเขตและจัดลำดับใหม่เป็นกลุ่มที่เล็กที่สุดเท่าที่จะเป็นไปได้ Haskell ทำสิ่งนี้จริง แต่ในภาษาเช่น F # (และ OCaml และ SML) ซึ่งมีผลข้างเคียงที่ไม่ จำกัด นี่เป็นความคิดที่ไม่ดีเพราะอาจจัดลำดับผลข้างเคียงใหม่ด้วย ดังนั้นแทนที่จะขอให้ผู้ใช้ทำเครื่องหมายอย่างชัดเจนว่าคำจำกัดความใดที่เกิดซ้ำร่วมกันและด้วยส่วนขยายที่ควรเกิดการวางนัยทั่วไป


3
เอ่อไม่ ย่อหน้าแรกของคุณไม่ถูกต้อง (คุณกำลังพูดถึงการใช้ "และ" อย่างชัดเจนไม่ใช่ "rec") ดังนั้นส่วนที่เหลือจึงไม่เกี่ยวข้อง
JD

5
ฉันไม่เคยพอใจกับข้อกำหนดนี้ ขอบคุณสำหรับคำอธิบาย อีกเหตุผลหนึ่งที่ Haskell เหนือกว่าในด้านการออกแบบ
Bent Rasmussen

9
ไม่ !!!! จะเกิดขึ้นได้อย่างไร! คำตอบนี้ไม่ถูกต้อง! โปรดอ่านคำตอบของ Harrop ด้านล่างหรือดูคำจำกัดความของ Standard ML (Milner, Tofte, Harper, MacQueen - 1997) [น. 24]
lambdapower

9
ดังที่ฉันได้กล่าวไว้ในคำตอบของฉันปัญหาการอนุมานประเภทเป็นหนึ่งในสาเหตุของความจำเป็นในการรับรู้แทนที่จะเป็นเหตุผลเดียว คำตอบของจอนก็เป็นคำตอบที่ถูกต้องเช่นกัน (นอกเหนือจากความคิดเห็นปกติเกี่ยวกับ Haskell); ฉันไม่คิดว่าทั้งสองจะขัดแย้งกัน
GS - ขอโทษโมนิกา

16
"ปัญหาการอนุมานประเภทเป็นสาเหตุหนึ่งของความจำเป็นในการบันทึก" ความจริงที่ว่า OCaml ต้องการrecแต่ SML ไม่ได้เป็นตัวอย่างที่ชัดเจน หากการอนุมานประเภทเป็นปัญหาด้วยเหตุผลที่คุณอธิบาย OCaml และ SML ไม่สามารถเลือกวิธีแก้ปัญหาที่แตกต่างกันได้ แน่นอนว่าเหตุผลก็คือคุณกำลังพูดถึงandเพื่อให้ Haskell มีความเกี่ยวข้อง
JD

10

มีเหตุผลสำคัญสองประการที่เป็นความคิดที่ดี:

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

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


4

บางส่วนเดา:

  • letไม่เพียงใช้เพื่อผูกฟังก์ชันเท่านั้น แต่ยังรวมถึงค่าปกติอื่น ๆ ด้วย รูปแบบของค่าส่วนใหญ่ไม่ได้รับอนุญาตให้เรียกซ้ำ อนุญาตให้ใช้ค่าแบบวนซ้ำบางรูปแบบได้ (เช่นฟังก์ชันการแสดงออกที่ขี้เกียจ ฯลฯ ) ดังนั้นจึงต้องมีไวยากรณ์ที่ชัดเจนเพื่อระบุสิ่งนี้
  • การปรับแต่งฟังก์ชันที่ไม่เรียกซ้ำอาจง่ายกว่า
  • การปิดที่สร้างขึ้นเมื่อคุณสร้างฟังก์ชันเรียกซ้ำจำเป็นต้องมีรายการที่ชี้ไปยังฟังก์ชันนั้น ๆ (เพื่อให้ฟังก์ชันสามารถเรียกตัวเองแบบวนซ้ำได้) ซึ่งทำให้การปิดแบบเรียกซ้ำซับซ้อนกว่าการปิดแบบไม่เรียกซ้ำ ดังนั้นจึงอาจเป็นการดีที่จะสามารถสร้างการปิดแบบไม่เรียกซ้ำได้ง่ายขึ้นเมื่อคุณไม่ต้องการการเรียกซ้ำ
  • ช่วยให้คุณสามารถกำหนดฟังก์ชันในรูปแบบของฟังก์ชันที่กำหนดไว้ก่อนหน้านี้หรือค่าของชื่อเดียวกัน แม้ว่าฉันคิดว่านี่เป็นการปฏิบัติที่ไม่ดี
  • เสริมความปลอดภัย? ตรวจสอบให้แน่ใจว่าคุณกำลังทำในสิ่งที่คุณตั้งใจไว้ เช่นถ้าคุณไม่ได้ตั้งใจให้มันเรียกซ้ำ แต่บังเอิญใช้ชื่อในฟังก์ชันที่มีชื่อเดียวกับตัวฟังก์ชันเองก็มักจะบ่น (เว้นแต่จะมีการกำหนดชื่อไว้ก่อน)
  • letสร้างคล้ายกับletการสร้างเสียงกระเพื่อมและโครงการ; ซึ่งไม่เกิดซ้ำ มีโครงสร้างแยกต่างหากletrecใน Scheme สำหรับการเรียกซ้ำ Let's

"รูปแบบของค่าส่วนใหญ่ไม่ได้รับอนุญาตให้เรียกซ้ำได้อนุญาตให้ใช้ค่าแบบวนซ้ำบางรูปแบบได้ (เช่นฟังก์ชันนิพจน์ lazy เป็นต้น) ดังนั้นจึงต้องมีไวยากรณ์ที่ชัดเจนเพื่อระบุสิ่งนี้" นั่นคือความจริงสำหรับ F # แต่ฉันไม่แน่ใจว่าจริงก็คือสำหรับ OCaml let rec xs = 0::ys and ys = 1::xsที่คุณสามารถทำได้
JD

4

ให้สิ่งนี้:

let f x = ... and g y = ...;;

เปรียบเทียบ:

let f a = f (g a)

ด้วยสิ่งนี้:

let rec f a = f (g a)

อดีตนิยามใหม่fที่จะใช้ที่กำหนดไว้ก่อนหน้านี้fถึงผลของการใช้เพื่อg aคำจำกัดความหลังใหม่fให้ใช้ loop ตลอดgไปaซึ่งโดยปกติจะไม่ใช่สิ่งที่คุณต้องการในตัวแปร ML

ที่กล่าวว่ามันเป็นสไตล์ของนักออกแบบภาษา เพียงแค่ไปกับมัน


1

ส่วนใหญ่คือช่วยให้โปรแกรมเมอร์สามารถควบคุมความซับซ้อนของขอบเขตในพื้นที่ได้มากขึ้น สเปกตรัมของlet, let*และlet recข้อเสนอในระดับที่เพิ่มขึ้นของทั้งพลังงานและค่าใช้จ่าย let*และlet recอยู่ในเวอร์ชันที่ซ้อนกันของ Simpleletดังนั้นการใช้อย่างใดอย่างหนึ่งจึงมีราคาแพงกว่า การให้คะแนนนี้ช่วยให้คุณปรับแต่งการเพิ่มประสิทธิภาพโปรแกรมของคุณได้ในระดับเล็กเพราะคุณสามารถเลือกระดับที่คุณต้องการสำหรับงานในมือได้ หากคุณไม่ต้องการการเรียกซ้ำหรือความสามารถในการอ้างถึงการเชื่อมโยงก่อนหน้านี้คุณสามารถถอยกลับไปง่ายๆเพื่อประหยัดประสิทธิภาพเล็กน้อย

คล้ายกับเพรดิเคตความเท่าเทียมกันที่ให้คะแนนใน Scheme (เช่นeq?, eqv?และequal?)

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