ลักษณะการทำงานของ foldl กับ foldr กับรายการที่ไม่มีที่สิ้นสุด


124

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

ฉันเขียนใหม่โดยใช้ foldl:

myAny :: (a -> Bool) -> [a] -> Bool
myAny p list = foldl step False list
   where
      step acc item = p item || acc

(โปรดสังเกตว่าอาร์กิวเมนต์ของฟังก์ชัน step จะถูกย้อนกลับอย่างถูกต้อง)

อย่างไรก็ตามจะไม่หยุดประมวลผลรายการที่ไม่มีที่สิ้นสุดอีกต่อไป

ฉันพยายามติดตามการดำเนินการของฟังก์ชันตามคำตอบของ Apocalisp :

myAny even [1..]
foldl step False [1..]
step (foldl step False [2..]) 1
even 1 || (foldl step False [2..])
False  || (foldl step False [2..])
foldl step False [2..]
step (foldl step False [3..]) 2
even 2 || (foldl step False [3..])
True   || (foldl step False [3..])
True

อย่างไรก็ตามนี่ไม่ใช่ลักษณะการทำงานของฟังก์ชัน นี่มันผิดยังไง?

คำตอบ:


231

ความfoldแตกต่างนั้นดูเหมือนจะเป็นสาเหตุของความสับสนอยู่บ่อยครั้งดังนั้นนี่คือภาพรวมทั่วไป:

พิจารณาพับรายการของค่าเอ็น[x1, x2, x3, x4 ... xn ]ที่มีฟังก์ชั่นบางอย่างและเมล็ดfz

foldl คือ:

  • การเชื่อมโยงด้านซ้าย :f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
  • หางแบบวนซ้ำ : มันวนซ้ำผ่านรายการสร้างมูลค่าในภายหลัง
  • ขี้เกียจ : ไม่มีการประเมินผลจนกว่าจะต้องการผลลัพธ์
  • ย้อนกลับ : ย้อนfoldl (flip (:)) []กลับรายการ

foldr คือ:

  • การเชื่อมโยงที่ถูกต้อง :f x1 (f x2 (f x3 (f x4 ... (f xn z) ... )))
  • วนซ้ำเป็นอาร์กิวเมนต์ : การวนซ้ำแต่ละครั้งใช้fกับค่าถัดไปและผลของการพับส่วนที่เหลือของรายการ
  • ขี้เกียจ : ไม่มีการประเมินผลจนกว่าจะต้องการผลลัพธ์
  • ส่งต่อ : foldr (:) []ส่งคืนรายการที่ไม่เปลี่ยนแปลง

มีประเด็นที่ละเอียดอ่อนเล็กน้อยนี่ที่เดินทางคนขึ้นไปบางครั้งเพราะfoldlเป็นข้างหลังแอพลิเคชันของแต่ละfจะถูกเพิ่มในนอกของผล; และเนื่องจากขี้เกียจจึงไม่มีการประเมินผลใด ๆ จนกว่าจะได้ผลลัพธ์ที่ต้องการ ซึ่งหมายความว่าในการคำนวณส่วนใดส่วนหนึ่งของผลลัพธ์ก่อนอื่น Haskell จะวนซ้ำผ่านรายการทั้งหมดโดยสร้างนิพจน์ของแอปพลิเคชันฟังก์ชันที่ซ้อนกันจากนั้นประเมินฟังก์ชันด้านนอกสุดประเมินอาร์กิวเมนต์ตามต้องการ ถ้าfมักจะใช้อาร์กิวเมนต์แรกนี้หมายถึง Haskell มีการ recurse fทุกทางลงไปที่ระยะสุดแล้วทำงานย้อนกลับคอมพิวเตอร์แอพลิเคชันของแต่ละ

เห็นได้ชัดว่านี่เป็นหนทางไกลจากโปรแกรมเมอร์ที่ใช้งานได้อย่างมีประสิทธิภาพส่วนใหญ่รู้จักและชื่นชอบ!

ในความเป็นจริงแม้ว่าจะfoldlเป็นแบบหางซ้ำในทางเทคนิคเนื่องจากนิพจน์ผลลัพธ์ทั้งหมดถูกสร้างขึ้นก่อนที่จะประเมินสิ่งใดก็ตามfoldlอาจทำให้เกิดสแต็กล้น!

บนมืออื่น ๆ foldrให้พิจารณา มันขี้เกียจเช่นกัน แต่เนื่องจากมันวิ่งไปข้างหน้าแอพพลิเคชั่นแต่ละตัวfจะถูกเพิ่มเข้าไปด้านในของผลลัพธ์ ดังนั้นในการคำนวณผลลัพธ์ Haskell จึงสร้างแอปพลิเคชันฟังก์ชันเดียวอาร์กิวเมนต์ที่สองซึ่งเป็นส่วนที่เหลือของรายการที่พับไว้ ถ้าfขี้เกียจในอาร์กิวเมนต์ที่สองเช่นตัวสร้างข้อมูล - ผลลัพธ์จะขี้เกียจเพิ่มขึ้นโดยแต่ละขั้นตอนของการพับจะคำนวณเฉพาะเมื่อมีการประเมินบางส่วนของผลลัพธ์ที่ต้องการเท่านั้น

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

ดังนั้นสองประเด็นสำคัญที่ควรทราบมีดังนี้:

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

คุณอาจสังเกตเห็นว่าดูเหมือนfoldrทำได้ทุกอย่างfoldlและอื่น ๆ อีกมากมาย นี่คือเรื่องจริง! ในความเป็นจริงพับเกือบจะไม่มีประโยชน์!

แต่ถ้าเราต้องการสร้างผลลัพธ์ที่ไม่ขี้เกียจโดยการพับรายการขนาดใหญ่ (แต่ไม่สิ้นสุด) ล่ะ? สำหรับสิ่งนี้เราต้องการการพับที่เข้มงวดซึ่งแม้ว่าไลบรารีมาตรฐานจะมีให้ :

foldl' คือ:

  • การเชื่อมโยงด้านซ้าย :f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
  • หางแบบวนซ้ำ : มันวนซ้ำผ่านรายการสร้างมูลค่าในภายหลัง
  • เข้มงวด : แอปพลิเคชันฟังก์ชันแต่ละตัวจะได้รับการประเมินระหว่างทาง
  • ย้อนกลับ : ย้อนfoldl' (flip (:)) []กลับรายการ

เนื่องจากfoldl'มีความเข้มงวดในการคำนวณผลลัพธ์ Haskell จะประเมิน fในแต่ละขั้นตอนแทนที่จะปล่อยให้อาร์กิวเมนต์ด้านซ้ายสะสมนิพจน์ขนาดใหญ่ที่ไม่มีการประเมิน สิ่งนี้ทำให้เรามีการเรียกหางซ้ำตามปกติและมีประสิทธิภาพที่เราต้องการ! กล่าวอีกนัยหนึ่ง:

  • foldl' สามารถพับรายการขนาดใหญ่ได้อย่างมีประสิทธิภาพ
  • foldl' จะค้างอยู่ในลูปที่ไม่มีที่สิ้นสุด (ไม่ทำให้สแตกล้น) ในรายการที่ไม่มีที่สิ้นสุด

Haskell wiki มีหน้าที่พูดถึงเรื่องนี้เช่นกัน


6
ฉันมาที่นี่เพราะฉันอยากรู้ว่าทำไมถึงfoldrดีกว่าfoldlในHaskellในขณะที่ตรงกันข้ามเป็นจริงในErlang (ซึ่งฉันเรียนรู้มาก่อนHaskell ) เนื่องจากErlangไม่ใช่คนขี้เกียจและฟังก์ชั่นก็ไม่ได้โค้งงอดังนั้นfoldlในErlang จึงมีพฤติกรรมเหมือนfoldl'ข้างบน นี่ตอบโจทย์มาก! ทำได้ดีมากและขอบคุณ!
Siu Ching Pong -Asuka Kenji-

7
ส่วนใหญ่เป็นคำอธิบายที่ดี แต่ฉันพบว่าคำอธิบายfoldl"ถอยหลัง" และfoldrเป็น "ไปข้างหน้า" มีปัญหา ส่วนนี้เป็นเพราะflipถูกนำไปใช้กับ(:)ในภาพประกอบที่ว่าทำไมการพับจึงถอยหลัง ปฏิกิริยาตามธรรมชาติคือ "แน่นอนว่ามันล้าหลัง: คุณflipเชื่อมต่อรายการเหยียบ!" นอกจากนี้ยังแปลกที่เห็นว่า "ย้อนกลับ" เนื่องจากfoldlใช้fกับองค์ประกอบรายการแรกก่อน (ด้านในสุด) ในการประเมินผลทั้งหมด นั่นคือfoldr"วิ่งย้อนกลับ" โดยfจะนำไปใช้กับองค์ประกอบสุดท้ายก่อน
Dave Abrahams

1
@DaveAbrahams: ระหว่างความยุติธรรมfoldlและการfoldrเพิกเฉยต่อความเข้มงวดและการเพิ่มประสิทธิภาพอันดับแรกหมายถึง "ด้านนอกสุด" ไม่ใช่ "ด้านในสุด" นี่คือเหตุผลที่foldrสามารถประมวลผลรายการอนันต์และfoldlลาดเท - ขวาพับแรกใช้fไปยังองค์ประกอบรายการแรกและ (unevaluated) fผลจากการพับหางขณะที่พับด้านซ้ายต้องสำรวจรายชื่อทั้งหมดที่จะประเมินผลการประยุกต์ใช้นอกสุดของ
CA McCann

1
ฉันแค่สงสัยว่ามีอินสแตนซ์ใดบ้างที่ควรใช้ foldl มากกว่า foldl 'คุณคิดว่ามีหรือไม่?
kazuoua

1
@kazuoua ที่ความขี้เกียจเป็นสิ่งสำคัญเช่น last xs = foldl (\a z-> z) undefined xs .
Will Ness

28
myAny even [1..]
foldl step False [1..]
foldl step (step False 1) [2..]
foldl step (step (step False 1) 2) [3..]
foldl step (step (step (step False 1) 2) 3) [4..]

เป็นต้น

โดยสัญชาตญาณfoldlมักจะอยู่ที่ "ด้านนอก" หรือ "ด้านซ้าย" ดังนั้นจึงได้รับการขยายก่อน โฆษณา infinitum


10

คุณสามารถดูได้ในเอกสารของ Haskell ที่นี่ว่า foldl เป็น tail-recursive และจะไม่สิ้นสุดหากผ่านรายการที่ไม่มีที่สิ้นสุดเนื่องจากจะเรียกตัวเองในพารามิเตอร์ถัดไปก่อนที่จะส่งคืนค่า ...


0

ฉันไม่รู้จัก Haskell แต่ใน Scheme fold-rightจะ 'ดำเนินการ' ในองค์ประกอบสุดท้ายของรายการก่อนเสมอ ดังนั้นจะไม่ทำงานสำหรับรายการแบบวนรอบ (ซึ่งเหมือนกับรายการที่ไม่มีที่สิ้นสุด)

ฉันไม่แน่ใจว่าfold-rightสามารถเขียน tail-recursive ได้หรือไม่ แต่สำหรับรายการแบบวนรอบคุณควรจะได้ stack overflow fold-leftโดยปกติแล้ว OTOH จะถูกนำไปใช้กับการเรียกซ้ำของหางและจะติดอยู่ในลูปที่ไม่มีที่สิ้นสุดหากไม่ยุติก่อน


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