Fibonacci-function นี้จำได้อย่างไร?


114

fibonacci-function นี้บันทึกโดยกลไกใด?

fib = (map fib' [0..] !!)                 
     where fib' 1 = 1                                                        
           fib' 2 = 1                                                        
           fib' n = fib (n-2) + fib (n-1)                    

และในบันทึกที่เกี่ยวข้องเหตุใดเวอร์ชันนี้จึงไม่เป็นเช่นนั้น

fib n = (map fib' [0..] !! n)                                               
     where fib' 1 = 1                                                        
           fib' 2 = 1                                                        
           fib' n = fib (n-2) + fib (n-1)                    

13
เล็กน้อย unrelatedly, fib 0ไม่ยุติ: คุณอาจต้องการกรณีฐานในการfib'ที่จะเป็นและfib' 0 = 0 fib' 1 = 1
huon

1
โปรดทราบว่าเวอร์ชันแรกสามารถทำให้รัดกุมมากขึ้น: fibs = 1:1:zipWith (+) fibs (tail fibs)และfib = (fibs !!).
Bastian

คำตอบ:


95

กลไกการประเมินผลใน Haskell เป็นไปตามความต้องการ : เมื่อต้องการค่าจะคำนวณและเตรียมไว้ให้พร้อมในกรณีที่มีการร้องขออีกครั้ง หากเรากำหนดรายการบางรายการxs=[0..]และถามหาองค์ประกอบxs!!99ที่ 100 ในภายหลังช่องที่ 100 ในรายการจะเป็น "เนื้อออก" โดยถือหมายเลขไว้99ตอนนี้พร้อมสำหรับการเข้าถึงครั้งต่อไป

นั่นคือสิ่งที่กลอุบาย "จะผ่านรายการ" คือการหาประโยชน์ ในนิยามฟีโบนักชีแบบทวีคูณตามปกติfib n = fib (n-1) + fib (n-2)ฟังก์ชันจะถูกเรียกสองครั้งจากด้านบนทำให้เกิดการระเบิดแบบเอ็กซ์โพเนนเชียล แต่ด้วยเคล็ดลับดังกล่าวเราได้กำหนดรายการสำหรับผลลัพธ์ระหว่างกาลและไปที่ "ผ่านรายการ":

fib n = (xs!!(n-1)) + (xs!!(n-2)) where xs = 0:1:map fib [2..]

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


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

ในเวอร์ชันแรกคอมไพเลอร์มีน้ำใจกับเราโดยนำนิพจน์ย่อยคงที่ ( map fib' [0..]) ออกและทำให้เป็นเอนทิตีที่แชร์ได้แยกกัน แต่ไม่อยู่ภายใต้ภาระผูกพันใด ๆ ที่จะต้องทำเช่นนั้น และมีหลายกรณีที่เราไม่ต้องการให้ทำเช่นนั้นโดยอัตโนมัติ

( แก้ไข :) พิจารณาการเขียนซ้ำเหล่านี้:

fib1 = f                     fib2 n = f n                 fib3 n = f n          
 where                        where                        where                
  f i = xs !! i                f i = xs !! i                f i = xs !! i       
  xs = map fib' [0..]          xs = map fib' [0..]          xs = map fib' [0..] 
  fib' 1 = 1                   fib' 1 = 1                   fib' 1 = 1          
  fib' 2 = 1                   fib' 2 = 1                   fib' 2 = 1          
  fib' i=fib1(i-2)+fib1(i-1)   fib' i=fib2(i-2)+fib2(i-1)   fib' i=f(i-2)+f(i-1)

ดังนั้นเรื่องจริงดูเหมือนจะเกี่ยวกับการกำหนดขอบเขตที่ซ้อนกัน ไม่มีขอบเขตภายนอกที่มีคำจำกัดความที่ 1 และ 3 ระวังอย่าเรียกขอบเขตภายนอกfib3แต่เป็นระดับเดียวกันfเดียวกัน

การเรียกใหม่แต่ละครั้งfib2ดูเหมือนจะสร้างคำจำกัดความที่ซ้อนกันขึ้นมาใหม่เนื่องจากสิ่งใดก็ตามที่สามารถกำหนดได้ (ในทางทฤษฎี) แตกต่างกันไปขึ้นอยู่กับค่าของn(ขอบคุณ Vitus และ Tikhon ที่ชี้ให้เห็น) ด้วยคำจำกัดความแรกจะไม่nต้องขึ้นอยู่กับและด้วยครั้งที่สามมีการพึ่งพา แต่การเรียกแต่ละครั้งแยกกันเพื่อfib3เรียกfซึ่งระมัดระวังในการเรียกคำจำกัดความจากขอบเขตระดับเดียวกันเท่านั้นภายในไปยังการเรียกใช้เฉพาะนี้fib3ดังนั้นจึงxsได้รับสิ่งเดียวกันใช้ซ้ำ (กล่าวคือใช้ร่วมกัน) สำหรับการเรียกใช้fib3 .

แต่ไม่มีอะไรติ๊ดคอมไพเลอร์จากการรับรู้ว่าคำจำกัดความภายในใด ๆ ของรุ่นดังกล่าวข้างต้นในความเป็นจริงที่เป็นอิสระของนอกnผูกพันที่จะดำเนินการยกแลมบ์ดาหลังจากทั้งหมดที่เกิดขึ้นใน memoization เต็ม (ยกเว้นคำจำกัดความ polymorphic) ในความเป็นจริงนั่นคือสิ่งที่เกิดขึ้นกับทั้งสามเวอร์ชันเมื่อประกาศด้วยชนิด monomorphic และคอมไพล์ด้วยแฟล็ก -O2 ด้วยการประกาศประเภทโพลีมอร์ฟิกจะfib3แสดงการแบ่งปันในพื้นที่และfib2ไม่มีการแบ่งปันเลย

ท้ายที่สุดแล้วขึ้นอยู่กับคอมไพเลอร์และการปรับแต่งคอมไพเลอร์ที่ใช้และวิธีที่คุณทดสอบ (การโหลดไฟล์ใน GHCI คอมไพล์หรือไม่ด้วย -O2 หรือไม่หรือแบบสแตนด์อโลน) และไม่ว่าจะได้รับ monomorphic หรือ polymorphic ชนิดที่พฤติกรรมอาจ เปลี่ยนแปลงอย่างสมบูรณ์ - ไม่ว่าจะแสดงการแบ่งปันในพื้นที่ (ต่อการโทร) (เช่นเวลาเชิงเส้นในการโทรแต่ละครั้ง) การบันทึก (เช่นเวลาเชิงเส้นในการโทรครั้งแรกและ 0 ครั้งในการโทรครั้งต่อไปที่มีอาร์กิวเมนต์เดียวกันหรือน้อยกว่า) หรือไม่มีการแบ่งปันเลย ( เวลาเอกซ์โพเนนเชียล)

คำตอบสั้น ๆ คือมันเป็นสิ่งที่รวบรวม :)


4
เพียงเพื่อแก้ไขรายละเอียดเล็ก ๆ น้อย ๆ : รุ่นที่สองไม่ได้รับการแบ่งปันใด ๆ ส่วนใหญ่เป็นเพราะการทำงานของท้องถิ่นfib'เป็นนิยามใหม่สำหรับทุกคนnและทำให้fib'ในfib 1fib'ในfib 2ซึ่งหมายถึงรายการที่แตกต่างกัน แม้ว่าคุณจะแก้ไขประเภทให้เป็น monomorphic แต่ก็ยังแสดงพฤติกรรมนี้
Vitus

1
whereประโยคแนะนำการแบ่งปันเหมือนletนิพจน์ แต่มักจะซ่อนปัญหาเช่นนี้ เขียนใหม่ให้ชัดเจนขึ้นอีกเล็กน้อยคุณจะได้รับสิ่งนี้: hpaste.org/71406
Vitus

1
อีกประเด็นที่น่าสนใจเกี่ยวกับการเขียนซ้ำของคุณ: ถ้าคุณให้ประเภท monomorphic (เช่นInt -> Integer) จากนั้นfib2จะทำงานในเวลาเอกซ์โพเนนเชียลfib1และfib3ทั้งคู่ทำงานในเวลาเชิงเส้น แต่fib1จะถูกบันทึกไว้ด้วย - อีกครั้งเพราะสำหรับfib3nคำจำกัดความของท้องถิ่นที่มีนิยามใหม่สำหรับทุกคน
Vitus

1
@misterbee แต่คงจะดีหากได้รับการรับรองจากผู้รวบรวม การควบคุมหน่วยความจำที่อยู่อาศัยของเอนทิตีเฉพาะบางประเภท บางครั้งเราต้องการแบ่งปันบางครั้งเราต้องการป้องกัน ฉันคิดว่า / หวังว่ามันน่าจะเป็นไปได้ ...
Will Ness

1
@ElizaBrandt สิ่งที่ฉันหมายถึงคือบางครั้งเราต้องการคำนวณบางสิ่งที่หนักหน่วงดังนั้นจึงไม่ถูกเก็บไว้ในหน่วยความจำสำหรับเรานั่นคือค่าใช้จ่ายในการคำนวณใหม่จะต่ำกว่าค่าใช้จ่ายในการเก็บหน่วยความจำมาก ตัวอย่างหนึ่งคือการสร้าง powerset: ในpwr (x:xs) = pwr xs ++ map (x:) pwr xs ; pwr [] = [[]]เราต้องการpwr xsคำนวณอย่างอิสระสองครั้งดังนั้นจึงสามารถรวบรวมขยะได้ทันทีในขณะที่กำลังผลิตและบริโภค
Will Ness

23

ฉันไม่แน่ใจทั้งหมด แต่นี่คือการคาดเดาที่มีการศึกษา:

คอมไพเลอร์สันนิษฐานว่าfib nอาจแตกต่างกันในรายการอื่นnดังนั้นจึงจำเป็นต้องคำนวณรายการใหม่ทุกครั้ง บิตภายในwhereคำสั่งอาจขึ้นอยู่กับnหลังจากทั้งหมด nนั่นคือในกรณีนี้รายชื่อทั้งหมดของตัวเลขเป็นหลักการทำงานของ

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

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

โดยพื้นฐานแล้วเป็นรูปแบบการเขียนโปรแกรมแบบไดนามิกที่ชาญฉลาดและให้เช่าน้อยโดยอาศัยความหมายขี้เกียจของ GHC ฉันคิดว่ามาตรฐานระบุเพียงว่าต้องไม่เข้มงวดดังนั้นคอมไพเลอร์ที่เข้ากันได้อาจรวบรวมโค้ดนี้เพื่อไม่ให้จดจำได้ อย่างไรก็ตามในทางปฏิบัติคอมไพเลอร์ที่สมเหตุสมผลทุกคนจะขี้เกียจ

สำหรับข้อมูลเพิ่มเติมเกี่ยวกับสาเหตุกรณีที่สองทำงานที่ทุกคนอ่านทำความเข้าใจเกี่ยวกับรายการที่กำหนดไว้ซ้ำ (FIBS ในแง่ของ zipWith)


คุณหมายความว่า " fib' nอาจแตกต่างจากที่อื่นn" หรือเปล่า
Will Ness

ฉันคิดว่าฉันไม่ได้ชัดเจนมาก: สิ่งที่ผมหมายถึงคือการที่ภายในทุกอย่างfibรวมทั้งอาจจะแตกต่างกันในทุกที่แตกต่างกันfib' nฉันคิดว่าตัวอย่างต้นฉบับค่อนข้างสับสนเล็กน้อยเพราะfib'ขึ้นอยู่กับnเงาของมันเองnด้วย
Tikhon Jelvis

20

ขั้นแรกด้วย ghc-7.4.2 คอมไพล์ด้วย -O2เวอร์ชันที่ไม่ได้บันทึกนั้นไม่ได้แย่ขนาดนี้รายการภายในของหมายเลข Fibonacci ยังคงถูกบันทึกไว้สำหรับการเรียกใช้ฟังก์ชันระดับบนสุดแต่ละครั้ง แต่มันไม่ใช่และไม่สามารถบันทึกได้อย่างสมเหตุสมผลในการโทรระดับบนสุดที่แตกต่างกัน อย่างไรก็ตามสำหรับเวอร์ชันอื่นรายการจะแชร์ระหว่างการโทร

นั่นเป็นเพราะข้อ จำกัด ของโมโนมอร์ฟิซึม

ประการแรกถูกผูกไว้ด้วยการผูกรูปแบบง่าย ๆ (เฉพาะชื่อไม่มีข้อโต้แย้ง) ดังนั้นโดยข้อ จำกัด ของ monomorphism จะต้องได้รับชนิด monomorphic ประเภทที่อนุมานคือ

fib :: (Num n) => Int -> n

และข้อ จำกัด ดังกล่าวจะผิดนัด (ในกรณีที่ไม่มีการประกาศเริ่มต้นที่บอกเป็นอย่างอื่น) เพื่อIntegerแก้ไขประเภทเป็น

fib :: Int -> Integer

ดังนั้นจึงมีเพียงรายการเดียว (ประเภท[Integer]) ที่จะช่วยจำ

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

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


เหตุใดจึงไม่สามารถบันทึกรายการสำหรับแต่ละประเภทได้ โดยหลักการแล้ว GHC สามารถสร้างพจนานุกรม (เหมือนกับการเรียกฟังก์ชันที่ จำกัด คลาสประเภท) เพื่อให้มีรายการที่คำนวณบางส่วนสำหรับ Num แต่ละประเภทที่พบในระหว่างรันไทม์ได้หรือไม่
misterbee

1
@misterbee โดยหลักการแล้วมันสามารถทำได้ แต่ถ้าโปรแกรมเรียกfib 1000000ใช้หลายประเภทสิ่งนั้นจะกินหน่วยความจำมาก เพื่อหลีกเลี่ยงสิ่งนั้นเราจำเป็นต้องมีฮิวริสติกซึ่งรายการที่จะโยนออกจากแคชเมื่อมีขนาดใหญ่เกินไป และกลยุทธ์การช่วยจำดังกล่าวจะนำไปใช้กับฟังก์ชันหรือค่าอื่น ๆ ด้วยเช่นกันดังนั้นคอมไพเลอร์จะต้องจัดการกับสิ่งต่าง ๆ จำนวนมากในการบันทึกความทรงจำสำหรับหลายประเภท ฉันคิดว่ามันน่าจะเป็นไปได้ที่จะใช้การช่วยจำโพลีมอร์ฟิก (บางส่วน) ด้วยฮิวริสติกที่ดีพอสมควร แต่ฉันสงสัยว่ามันจะคุ้มค่า
Daniel Fischer

5

คุณไม่จำเป็นต้องมีฟังก์ชันบันทึกสำหรับ Haskell เฉพาะภาษาโปรแกรมเชิงประจักษ์เท่านั้นที่ต้องการฟังก์ชันนั้น อย่างไรก็ตาม Haskel เป็นภาษาที่ใช้งานได้และ ...

ดังนั้นนี่คือตัวอย่างของอัลกอริทึม Fibonacci ที่รวดเร็วมาก:

fib = zipWith (+) (0:(1:fib)) (1:fib)

zipWith เป็นฟังก์ชันจาก Prelude มาตรฐาน:

zipWith :: (a->b->c) -> [a]->[b]->[c]
zipWith op (n1:val1) (n2:val2) = (n1 + n2) : (zipWith op val1 val2)
zipWith _ _ _ = []

ทดสอบ:

print $ take 100 fib

เอาท์พุท:

[1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181,6765,10946,17711,28657,46368,75025,121393,196418,317811,514229,832040,1346269,2178309,3524578,5702887,9227465,14930352,24157817,39088169,63245986,102334155,165580141,267914296,433494437,701408733,1134903170,1836311903,2971215073,4807526976,7778742049,12586269025,20365011074,32951280099,53316291173,86267571272,139583862445,225851433717,365435296162,591286729879,956722026041,1548008755920,2504730781961,4052739537881,6557470319842,10610209857723,17167680177565,27777890035288,44945570212853,72723460248141,117669030460994,190392490709135,308061521170129,498454011879264,806515533049393,1304969544928657,2111485077978050,3416454622906707,5527939700884757,8944394323791464,14472334024676221,23416728348467685,37889062373143906,61305790721611591,99194853094755497,160500643816367088,259695496911122585,420196140727489673,679891637638612258,1100087778366101931,1779979416004714189,2880067194370816120,4660046610375530309,7540113804746346429,12200160415121876738,19740274219868223167,31940434634990099905,51680708854858323072,83621143489848422977,135301852344706746049,218922995834555169026,354224848179261915075,573147844013817084101]

เวลาที่ผ่านไป: 0.00018 วินาที


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