Ý nghĩa của Foldr so với Foldl (hoặc Foldl ')


155

Đầu tiên, Real World Haskell , mà tôi đang đọc, nói rằng không bao giờ sử dụng foldlvà thay vào đó sử dụng foldl'. Vì vậy, tôi tin tưởng nó.

Nhưng tôi không biết khi nào nên sử dụng foldrso với foldl'. Mặc dù tôi có thể thấy cấu trúc của cách chúng hoạt động khác nhau được đặt ra trước mặt tôi, tôi quá ngu ngốc để hiểu khi nào "cái nào tốt hơn". Tôi đoán có vẻ như tôi thực sự không nên sử dụng nó, vì cả hai đều đưa ra cùng một câu trả lời (phải không?). Trên thực tế, trải nghiệm trước đây của tôi với cấu trúc này là từ Ruby injectvà Clojure reduce, dường như không có phiên bản "trái" và "phải". (Câu hỏi bên lề: họ sử dụng phiên bản nào?)

Bất kỳ cái nhìn sâu sắc nào có thể giúp một loại thách thức thông minh như tôi sẽ được đánh giá cao!

Câu trả lời:


174

Đệ quy cho foldr f x ysnơi ys = [y1,y2,...,yk]trông giống như

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

trong khi đệ quy cho foldl f x yshình như

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

Một sự khác biệt quan trọng ở đây là nếu kết quả f x ycó thể được tính bằng cách chỉ sử dụng giá trị của xthì foldrkhông cần phải kiểm tra toàn bộ danh sách. Ví dụ

foldr (&&) False (repeat False)

trả lại Falsetrong khi

foldl (&&) False (repeat False)

không bao giờ chấm dứt. (Lưu ý: repeat Falsetạo một danh sách vô hạn trong đó mọi phần tử là False.)

Mặt khác, foldl'là đệ quy đuôi và nghiêm ngặt. Nếu bạn biết rằng bạn sẽ phải duyệt toàn bộ danh sách bất kể điều gì (ví dụ: tổng các số trong danh sách), thì foldl'sẽ hiệu quả hơn về không gian- (và có lẽ là thời gian-) foldr.


2
Trong Foldr, nó đánh giá là f y1 thunk, do đó, nó trả về Sai, tuy nhiên trong Foldl, f không thể biết một trong hai tham số của nó. Trong Haskell, cho dù nó có đệ quy hay không, cả hai đều có thể gây ra tràn. quá lớn. Foldl 'có thể giảm thunk ngay lập tức trong quá trình thực thi.
Sawyer

25
Để tránh nhầm lẫn, lưu ý rằng dấu ngoặc đơn không hiển thị thứ tự đánh giá thực tế. Vì Haskell lười biếng, các biểu thức ngoài cùng sẽ được đánh giá đầu tiên.
Lii

Greate trả lời. Tôi muốn thêm rằng nếu bạn muốn một nếp gấp có thể dừng một phần thông qua danh sách, bạn phải sử dụng Foldr; trừ khi tôi nhầm, các nếp gấp bên trái không thể dừng lại. (Bạn gợi ý điều này khi bạn nói "nếu bạn biết ... bạn sẽ ... duyệt toàn bộ danh sách"). Ngoài ra, lỗi đánh máy "chỉ sử dụng trên giá trị" nên được đổi thành "chỉ sử dụng giá trị". Tức là xóa từ "trên". (Stackoverflow sẽ không cho phép tôi gửi thay đổi 2 char!).
Lqueryvg

@Lqueryvg hai cách để dừng các nếp gấp bên trái: 1. viết mã bằng một nếp gấp bên phải (xem fodlWhile); 2. chuyển đổi nó thành quét trái ( scanl) và dừng nó với last . takeWhile phoặc tương tự. Uh, và 3. sử dụng mapAccumL. :)
Will Ness

1
@Desty bởi vì nó tạo ra phần mới của kết quả tổng thể của nó trên mỗi bước - không giống như foldl, nó thu thập kết quả chung của nó và chỉ tạo ra nó sau khi tất cả công việc kết thúc và không còn bước nào để thực hiện. Vì vậy, ví dụ foldl (flip (:)) [] [1..3] == [3,2,1], vì vậy scanl (flip(:)) [] [1..] = [[],[1],[2,1],[3,2,1],...]... IOW foldl f z xs = last (scanl f z xs)các danh sách vô hạn không có phần tử cuối cùng (mà trong ví dụ trên, bản thân nó sẽ là một danh sách vô hạn, từ INF xuống còn 1).
Will Ness

57

foldr trông như thế này:

Trực quan gấp phải

foldl trông như thế này:

Hình trực quan

Bối cảnh: Gấp trên wiki Haskell


33

Ngữ nghĩa của chúng khác nhau, do đó bạn không thể trao đổi foldlfoldr. Người này gấp các yếu tố từ bên trái, bên kia từ bên phải. Bằng cách đó, toán tử được áp dụng theo một thứ tự khác. Điều này quan trọng đối với tất cả các hoạt động không liên kết, chẳng hạn như trừ.

Haskell.org có một bài viết thú vị về chủ đề này.


Ngữ nghĩa của chúng chỉ khác nhau một cách tầm thường, đó là vô nghĩa trong thực tế: Thứ tự các đối số của hàm được sử dụng. Vì vậy, giao diện khôn ngoan họ vẫn tính là trao đổi. Sự khác biệt thực sự là, dường như, chỉ có tối ưu hóa / thực hiện.
Evi1M4chine

2
@ Evi1M4chine Không có sự khác biệt nào là tầm thường. Trái lại, chúng là đáng kể (và, vâng, có ý nghĩa trong thực tế). Trong thực tế, nếu tôi viết câu trả lời này ngày hôm nay, nó sẽ nhấn mạnh sự khác biệt này nhiều hơn nữa.
Konrad Rudolph

23

Một thời gian ngắn, foldrsẽ tốt hơn khi hàm tích lũy lười biếng trong đối số thứ hai của nó. Đọc thêm tại Stack Overflow của Haskell wiki (ý định chơi chữ).


18

Lý do foldl'được ưu tiên foldlcho 99% tất cả các mục đích sử dụng là vì nó có thể chạy trong không gian liên tục cho hầu hết các mục đích sử dụng.

Lấy chức năng sum = foldl['] (+) 0. Khi foldl'được sử dụng, tổng được tính ngay lập tức, do đó, việc áp dụng sumvào danh sách vô hạn sẽ chỉ chạy mãi mãi và rất có thể trong không gian không đổi (nếu bạn đang sử dụng những thứ như Ints, Doubles, Floats. IntegerS sẽ sử dụng nhiều hơn không gian không đổi nếu số trở nên lớn hơn maxBound :: Int).

Với foldl, một thunk được xây dựng (giống như một công thức làm thế nào để có được câu trả lời, có thể được đánh giá sau đó, thay vì lưu trữ câu trả lời). Những chiếc thun này có thể chiếm rất nhiều không gian, và trong trường hợp này, việc đánh giá biểu thức sẽ tốt hơn nhiều so với việc lưu trữ thunk (dẫn đến một ngăn xếp tràn ra và dẫn bạn đến với oh oh đừng bận tâm)

Mong rằng sẽ giúp.


1
Ngoại lệ lớn là nếu hàm được truyền vào foldlkhông làm gì ngoài việc áp dụng các hàm tạo cho một hoặc nhiều đối số của nó.
dfeuer

Có một mô hình chung để khi nào foldlthực sự là sự lựa chọn tốt nhất? (Giống như danh sách vô hạn khi foldrlựa chọn sai, tối ưu hóa khôn ngoan.?)
Evi1M4chine

@ Evi1M4chine không chắc ý của bạn là gì foldrkhi chọn sai cho danh sách vô hạn. Trong thực tế, bạn không nên sử dụng foldlhoặc foldl'cho danh sách vô hạn. Xem wiki Haskell trên tràn ngăn xếp
KevinOrr

14

Nhân tiện, Ruby injectvà Clojure reducefoldl(hoặc foldl1, tùy thuộc vào phiên bản bạn sử dụng). Thông thường, khi chỉ có một hình thức trong một ngôn ngữ, đó là một nếp gấp bên trái, bao gồm Python reduce, Perl List::Util::reduce, C ++ accumulate, C # Aggregate, Smalltalk inject:into:, PHP array_reduce, Mathicala Fold, v.v ... Mặc reduceđịnh của Lisp mặc định gấp bên trái


2
Nhận xét này là hữu ích nhưng tôi sẽ đánh giá cao các nguồn.
titandecoy

Lisp thông thường reducekhông lười biếng, vì vậy nó foldl'và phần lớn các cân nhắc ở đây không áp dụng.
MicroVirus

Tôi nghĩ bạn có nghĩa là foldl', vì chúng là ngôn ngữ nghiêm ngặt, phải không? Nếu không, điều đó có nghĩa là tất cả các phiên bản đó gây ra tràn ngăn xếp như thế foldlnào?
Evi1M4chine

6

Như Konrad chỉ ra, ngữ nghĩa của họ là khác nhau. Họ thậm chí không có cùng loại:

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

Ví dụ, toán tử nối thêm danh sách (++) có thể được thực hiện với foldrnhư

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

trong khi

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

sẽ cung cấp cho bạn một loại lỗi.


Loại của họ là như nhau, chỉ cần chuyển đổi xung quanh, đó là không liên quan. Giao diện và kết quả của chúng là như nhau, ngoại trừ vấn đề P / NP khó chịu (đọc: danh sách vô hạn). ;) Việc tối ưu hóa do thực hiện là sự khác biệt duy nhất trong thực tế, theo như tôi có thể nói.
Evi1M4chine

@ Evi1M4chine Điều này không chính xác, hãy xem ví dụ này: foldl subtract 0 [1, 2, 3, 4]đánh giá -10, trong khi foldr subtract 0 [1, 2, 3, 4]đánh giá -2. foldlthực sự là 0 - 1 - 2 - 3 - 4trong khi foldr4 - 3 - 2 - 1 - 0.
krapht

@krapht, foldr (-) 0 [1, 2, 3, 4]-2foldl (-) 0 [1, 2, 3, 4]-10. Mặt khác, subtractlà ngược với những gì bạn có thể mong đợi ( subtract 10 144), do đó foldr subtract 0 [1, 2, 3, 4]-10foldl subtract 0 [1, 2, 3, 4]2(tích cực).
pianoJames
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.