Sự fold
khác biệt như thế nào dường như là một nguồn thường xuyên gây nhầm lẫn, vì vậy đây là một tổng quan chung hơn:
Xem xét việc gấp một danh sách gồm n giá trị [x1, x2, x3, x4 ... xn ]
với một số hàm f
và hạt giống z
.
foldl
Là:
- Liên kết trái :
f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
- Đệ quy đuôi : Nó lặp lại qua danh sách, tạo ra giá trị sau đó
- Lười biếng : Không có gì được đánh giá cho đến khi cần kết quả
- Backwards :
foldl (flip (:)) []
đảo ngược danh sách.
foldr
Là:
- Liên kết phù hợp :
f x1 (f x2 (f x3 (f x4 ... (f xn z) ... )))
- Đệ quy thành một đối số : Mỗi lần lặp áp dụng
f
cho giá trị tiếp theo và kết quả của việc gấp phần còn lại của danh sách.
- Lười biếng : Không có gì được đánh giá cho đến khi cần kết quả
- Chuyển tiếp :
foldr (:) []
trả về một danh sách không thay đổi.
Có một điểm hơi tế nhị ở đây là chuyến đi người lên đôi: Bởi vì foldl
là ngược từng áp dụng f
sẽ được thêm vào bên ngoài của kết quả; và bởi vì nó lười biếng , không có gì được đánh giá cho đến khi kết quả được yêu cầu. Điều này có nghĩa là để tính toán bất kỳ phần nào của kết quả, đầu tiên Haskell lặp lại toàn bộ danh sách, xây dựng một biểu thức của các ứng dụng hàm lồng nhau, sau đó đánh giá hàm ngoài cùng , đánh giá các đối số của nó khi cần thiết. Nếu f
luôn sử dụng đối số đầu tiên của nó, điều này có nghĩa là Haskell phải đệ quy tất cả các cách xuống từ trong cùng, sau đó tính toán ngược lại từng ứng dụng của f
.
Điều này rõ ràng là khác xa so với đệ quy đuôi hiệu quả mà hầu hết các lập trình viên chức năng biết và yêu thích!
Trên thực tế, mặc dù foldl
về mặt kỹ thuật là đệ quy đuôi, bởi vì toàn bộ biểu thức kết quả được xây dựng trước khi đánh giá bất kỳ điều gì, foldl
có thể gây ra tràn ngăn xếp!
Mặt khác, hãy cân nhắc foldr
. Nó cũng lười biếng, nhưng vì nó chạy về phía trước , mỗi ứng dụng của f
được thêm vào bên trong kết quả. Vì vậy, để tính toán kết quả, Haskell xây dựng một ứng dụng hàm duy nhất , đối số thứ hai là phần còn lại của danh sách gấp. Nếu f
lười biếng trong đối số thứ hai của nó - ví dụ: một phương thức xây dựng dữ liệu - kết quả sẽ lười biếng dần dần , với mỗi bước của màn hình đầu tiên chỉ được tính khi một số phần của kết quả cần nó được đánh giá.
Vì vậy, chúng ta có thể thấy tại sao foldr
đôi khi lại hoạt động trên danh sách vô hạn khi foldl
thì không: Cái trước có thể chuyển đổi một cách lười biếng một danh sách vô hạn thành một cấu trúc dữ liệu vô hạn lười biếng khác, trong khi cái sau phải kiểm tra toàn bộ danh sách để tạo ra bất kỳ phần nào của kết quả. Mặt khác, foldr
với một hàm cần cả hai đối số ngay lập tức, chẳng hạn như (+)
, hoạt động (hay đúng hơn là không hoạt động) giống như foldl
, xây dựng một biểu thức lớn trước khi đánh giá nó.
Vì vậy, hai điểm quan trọng cần lưu ý là:
foldr
có thể biến đổi một cấu trúc dữ liệu đệ quy lười biếng thành một cấu trúc dữ liệu đệ quy khác.
- Nếu không, các nếp gấp lười biếng sẽ bị lỗi khi tràn ngăn xếp trên các danh sách lớn hoặc vô hạn.
Bạn có thể nhận thấy rằng có vẻ như foldr
có thể làm mọi thứ foldl
có thể, cộng với nhiều hơn thế nữa. Đây là sự thật! Trên thực tế, gập đôi gần như vô dụng!
Nhưng nếu chúng ta muốn tạo ra một kết quả không lười biếng bằng cách gấp lại một danh sách lớn (nhưng không phải là vô hạn) thì sao? Đối với điều này, chúng tôi muốn một nếp gấp nghiêm ngặt , mà các thư viện tiêu chuẩn cung cấp một cách khéo léo :
foldl'
Là:
- Liên kết trái :
f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
- Đệ quy đuôi : Nó lặp lại qua danh sách, tạo ra giá trị sau đó
- Nghiêm ngặt : Mỗi ứng dụng chức năng được đánh giá trong quá trình
- Backwards :
foldl' (flip (:)) []
đảo ngược danh sách.
Bởi vì foldl'
là nghiêm ngặt , để tính toán kết quả Haskell sẽ đánh giá f
tại mỗi bước, thay vì để cho các lập luận trái tích lũy một, biểu unevaluated khổng lồ. Điều này cho chúng ta phép đệ quy đuôi thông thường, hiệu quả mà chúng ta muốn! Nói cách khác:
foldl'
có thể gấp các danh sách lớn một cách hiệu quả.
foldl'
sẽ treo trong một vòng lặp vô hạn (không gây tràn ngăn xếp) trên một danh sách vô hạn.
Haskell wiki cũng có một trang thảo luận về vấn đề này .
foldr
tốt hơnfoldl
ở Haskell , trong khi ở Erlang thì ngược lại (mà tôi đã học trước Haskell ). Kể từ khi Erlang là không lười biếng và chức năng không được cà ri , vì vậyfoldl
trong Erlang cư xử nhưfoldl'
trên. Đây là một câu trả lời tuyệt vời! Công việc tốt và cảm ơn!