ผลกระทบของ foldr กับ foldl (หรือ foldl ')


155

ประการแรกโลกแห่งความจริง Haskellซึ่งฉันอ่านกล่าวว่าจะไม่เคยใช้และแทนที่จะใช้foldl foldl'ดังนั้นฉันจึงเชื่อมั่น

แต่ฉันมีหมอกในเมื่อจะใช้กับfoldr foldl'แม้ว่าฉันจะสามารถเห็นโครงสร้างของวิธีการทำงานของพวกเขาที่แตกต่างออกไปตรงหน้าฉันก็โง่เกินกว่าที่จะเข้าใจว่า "ซึ่งดีกว่า" ฉันคิดว่ามันดูเหมือนว่าฉันไม่ควรทำเรื่องสำคัญซึ่งใช้เพราะพวกเขาทั้งคู่ให้คำตอบเดียวกัน (ไม่ใช่พวกเขา?) ในความเป็นจริงประสบการณ์ก่อนหน้าของฉันกับโครงสร้างนี้มาจาก Ruby injectand Clojure's reduceซึ่งดูเหมือนจะไม่มีเวอร์ชั่น "ซ้าย" และ "ถูกต้อง" (คำถามด้านข้าง: พวกเขาใช้เวอร์ชันใด)

ข้อมูลเชิงลึกใด ๆ ที่สามารถช่วยคัดสรรที่ท้าทายเช่นฉันจะได้รับการชื่นชมมาก!

คำตอบ:


174

การสอบถามซ้ำสำหรับfoldr f x ysตำแหน่งที่ys = [y1,y2,...,yk]ดูเหมือน

f y1 (f y2 (... (f yk x) ...))

ในขณะที่การสอบถามซ้ำสำหรับfoldl f x ysดูเหมือนว่า

f (... (f (f x y1) y2) ...) yk

ข้อแตกต่างที่สำคัญคือถ้าผลลัพธ์ของf x yสามารถคำนวณได้โดยใช้ค่าเพียงอย่างเดียวxก็foldrไม่จำเป็นต้องตรวจสอบรายการทั้งหมด ตัวอย่างเช่น

foldr (&&) False (repeat False)

ผลตอบแทนFalseในขณะที่

foldl (&&) False (repeat False)

ไม่เคยยุติ (บันทึก:repeat Falseสร้างรายการที่ไม่มีที่สิ้นสุดซึ่งทุกองค์ประกอบอยู่False)

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


2
ใน foldr จะประเมินว่า f y1 thunk ดังนั้นจึงส่งกลับค่า False อย่างไรก็ตามใน foldl f ไม่สามารถรู้พารามิเตอร์ใด ๆ ของมันได้ใน Haskell ไม่ว่าจะเรียกซ้ำหางหรือไม่ก็ตาม มีขนาดใหญ่เกินไป. foldl 'สามารถลด thunk ได้ทันทีตามการดำเนินการ
Sawyer

25
เพื่อหลีกเลี่ยงความสับสนโปรดทราบว่าวงเล็บไม่แสดงลำดับการประเมินจริง เนื่องจาก Haskell ขี้เกียจการแสดงออกภายนอกสุดจะได้รับการประเมินก่อน
Lii

คำตอบที่ดี ฉันต้องการที่จะเพิ่มว่าถ้าคุณต้องการพับซึ่งสามารถหยุดส่วนทางผ่านรายการคุณต้องใช้ foldr; นอกจากฉันจะเข้าใจผิดแล้วไม่สามารถหยุดยั้งการพับด้านซ้าย (คุณบอกใบ้เรื่องนี้เมื่อคุณพูดว่า "ถ้าคุณรู้ ... คุณจะ ... สำรวจรายการทั้งหมด") นอกจากนี้ตัวพิมพ์ "โดยใช้เฉพาะค่า" ควรเปลี่ยนเป็น "ใช้เฉพาะค่า" คือลบคำว่า "on" (Stackoverflow จะไม่ให้ฉันส่งการเปลี่ยนแปลง 2 ถ่าน!)
Lqueryvg

@Lqueryvg สองวิธีในการหยุดการพับด้านซ้าย: 1. เขียนโค้ดด้วยการพับครึ่งขวา (ดูfodlWhile); 2. แปลงเป็นการสแกนทางซ้าย ( scanl) และหยุดสิ่งนั้นด้วยlast . takeWhile pหรือคล้ายกัน เอ่อ, และ 3. mapAccumLการใช้งาน :)
Will Ness

1
@Desty เนื่องจากสร้างส่วนใหม่ของผลลัพธ์โดยรวมในแต่ละขั้นตอนซึ่งต่างจากการfoldlรวบรวมผลลัพธ์โดยรวมและสร้างขึ้นหลังจากเสร็จสิ้นงานทั้งหมดและไม่มีขั้นตอนในการดำเนินการอีกต่อไป ดังนั้นเช่นfoldl (flip (:)) [] [1..3] == [3,2,1]ดังนั้นscanl (flip(:)) [] [1..] = [[],[1],[2,1],[3,2,1],...]... IOW, foldl f z xs = last (scanl f z xs)และรายการที่ไม่มีที่สิ้นสุดไม่มีองค์ประกอบสุดท้าย (ซึ่งในตัวอย่างข้างต้นตัวเองจะเป็นรายการที่ไม่มีที่สิ้นสุดจาก INF ลงไป 1)
Will Ness


33

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

Haskell.orgมีบทความที่น่าสนใจในเรื่องนี้


ความหมายของพวกเขาแตกต่างกันเล็กน้อยในทางที่ไม่มีความหมายในทางปฏิบัติ: ลำดับของการขัดแย้งของฟังก์ชันที่ใช้ ดังนั้นอินเทอร์เฟซที่ชาญฉลาดพวกเขายังคงนับเป็นแลกเปลี่ยน ความแตกต่างที่แท้จริงคือดูเหมือนว่าการเพิ่มประสิทธิภาพ / การใช้งานเท่านั้น
Evi1M4chine

2
@ Evi1M4chine ไม่มีความแตกต่างเล็กน้อย ในทางตรงกันข้ามพวกเขามีรูปธรรม (และใช่มีความหมายในทางปฏิบัติ) อันที่จริงถ้าฉันจะเขียนคำตอบนี้วันนี้มันจะเน้นความแตกต่างนี้มากยิ่งขึ้น
Konrad Rudolph

23

ไม่นานfoldrจะดีกว่าเมื่อฟังก์ชันตัวสะสมเป็นตัวขี้เกียจในอาร์กิวเมนต์ที่สอง อ่านเพิ่มเติมที่Stack Overflowของ Haskell wiki (ตั้งใจให้เล่น)


18

เหตุผลfoldl'เป็นที่ต้องการfoldlสำหรับ 99% ของการใช้งานทั้งหมดคือมันสามารถทำงานในพื้นที่คงที่สำหรับการใช้งานส่วนใหญ่

sum = foldl['] (+) 0ใช้ฟังก์ชั่น เมื่อfoldl'มีการใช้ผลรวมจะถูกคำนวณทันทีดังนั้นการนำsumไปใช้กับรายการที่ไม่มีที่สิ้นสุดจะทำงานตลอดไปและเป็นไปได้มากที่สุดในพื้นที่คงที่ (ถ้าคุณใช้สิ่งต่าง ๆ เช่นInts, Doubles, Floats. Integers จะใช้มากกว่าพื้นที่คงที่ถ้า จำนวนกลายเป็นใหญ่กว่าmaxBound :: Int )

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

หวังว่าจะช่วย


1
ข้อยกเว้นใหญ่คือถ้าฟังก์ชั่นที่ส่งผ่านไปfoldlไม่ทำอะไรเลย แต่ใช้ตัวสร้างกับข้อโต้แย้งของมันอย่างน้อยหนึ่งข้อ
dfeuer

มีรูปแบบทั่วไปfoldlหรือไม่เมื่อเป็นตัวเลือกที่ดีที่สุด? (เช่นเดียวกับรายการที่ไม่มีที่สิ้นสุดเมื่อfoldrเป็นตัวเลือกที่ผิด, การเพิ่มประสิทธิภาพที่ชาญฉลาด?)
Evi1M4chine

@ Evi1M4chine ไม่แน่ใจว่าคุณหมายถึงอะไรโดยfoldrการเลือกผิดสำหรับรายการที่ไม่มีที่สิ้นสุด ที่จริงแล้วคุณไม่ควรใช้foldlหรือfoldl'สำหรับรายการที่ไม่มีที่สิ้นสุด ดูวิกิ Haskell บนสแต็คโอเวอร์
โฟล

14

อย่างไรก็ตาม Ruby's injectและ Clojure ก็reduceคือfoldl(หรือfoldl1ขึ้นอยู่กับรุ่นที่คุณใช้) โดยปกติเมื่อมีเพียงรูปแบบเดียวในภาษามันเป็นรอยพับด้านซ้ายรวมถึง Python reduce, Perl's List::Util::reduce, C ++ 's accumulate, C #' s Aggregate, Smalltalk's inject:into:, PHP array_reduce, Mathematica ของFoldฯลฯreduceค่าเริ่มต้นของ Lisp สามัญเพื่อพับครึ่งซ้าย แต่มีตัวเลือกสำหรับการพับขวา


2
ความคิดเห็นนี้มีประโยชน์ แต่ฉันจะขอบคุณแหล่งที่มา
titaniumdecoy

Common LISP reduceไม่ได้ขี้เกียจดังนั้นจึงเป็นfoldl'เรื่องที่ควรคำนึงถึงเป็นอย่างมาก
MicroVirus

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

6

ตามที่คอนราดชี้ให้เห็นความหมายของพวกเขานั้นแตกต่างกัน พวกเขาไม่มีประเภทเดียวกัน:

ghci> :t foldr
foldr :: (a -> b -> b) -> b -> [a] -> b
ghci> :t foldl
foldl :: (a -> b -> a) -> a -> [b] -> a
ghci> 

ตัวอย่างเช่นตัวดำเนินการผนวกรายการ (++) สามารถนำไปใช้กับfoldras

(++) = flip (foldr (:))

ในขณะที่

(++) = flip (foldl (:))

จะทำให้คุณมีข้อผิดพลาดประเภท


ประเภทของพวกเขาเหมือนกันเพียงแค่สลับไปมาซึ่งไม่เกี่ยวข้อง ส่วนต่อประสานและผลลัพธ์ของพวกเขาเหมือนกันยกเว้นปัญหา P / NP ที่น่ารังเกียจ (อ่าน: รายการไม่สิ้นสุด) ;) การปรับให้เหมาะสมเนื่องจากการใช้งานเป็นเพียงความแตกต่างในทางปฏิบัติเท่าที่ฉันสามารถบอกได้
Evi1M4chine

@ Evi1M4chine นี้ไม่ถูกต้องดูที่ตัวอย่างนี้foldl subtract 0 [1, 2, 3, 4]ประเมิน-10ในขณะที่ประเมินfoldr subtract 0 [1, 2, 3, 4] เป็นจริงในขณะที่เป็น -2foldl0 - 1 - 2 - 3 - 4foldr4 - 3 - 2 - 1 - 0
krapht

@krapht, foldr (-) 0 [1, 2, 3, 4]เป็น-2และเป็นfoldl (-) 0 [1, 2, 3, 4] -10บนมืออื่น ๆ ที่subtractจะย้อนกลับจากสิ่งที่คุณอาจคาดหวัง ( subtract 10 14เป็น4) เพื่อfoldr subtract 0 [1, 2, 3, 4]เป็น-10และfoldl subtract 0 [1, 2, 3, 4]เป็น2(บวก)
pianoJames
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.