hành vi gập lại so với gấp với danh sách vô hạn


124

Mã cho hàm myAny trong câu hỏi này sử dụng trình gấp. Nó ngừng xử lý một danh sách vô hạn khi vị từ được thỏa mãn.

Tôi đã viết lại nó bằng cách sử dụng gấp:

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

(Lưu ý rằng các đối số của hàm bước được đảo ngược một cách chính xác.)

Tuy nhiên, nó không còn ngừng xử lý danh sách vô hạn.

Tôi đã cố gắng theo dõi quá trình thực thi của hàm như trong câu trả lời của 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

Tuy nhiên, đây không phải là cách hàm hoạt động. Làm thế nào là sai?

Câu trả lời:


231

Sự foldkhá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 fvà 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 fcho 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ì foldlngược từng áp dụng fsẽ đượ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 fluô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ù foldlvề 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ì, foldlcó 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 flườ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 foldlthì 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, foldrvớ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ư foldrcó thể làm mọi thứ foldlcó 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'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 .


6
Tôi đến đây vì tôi tò mò tại sao lại foldrtốt hơn foldlHaskell , 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ậy foldltrong 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!
Siu Ching Pong -Asuka Kenji- Ngày

7
Đây hầu hết là một lời giải thích tuyệt vời, nhưng tôi thấy mô tả foldllà "lùi" và foldr"tiến" có vấn đề. Điều này một phần fliplà do đang được áp dụng (:)trong hình minh họa tại sao nếp gấp bị lùi lại. Phản ứng tự nhiên là, "tất nhiên là nó lạc hậu: bạn flipnối danh sách!" Cũng thật kỳ lạ khi thấy điều đó được gọi là "lùi" vì foldláp dụng fcho phần tử danh sách đầu tiên trước tiên (trong cùng) trong một đánh giá hoàn chỉnh. Đó là foldr"chạy lùi", áp dụng fcho phần tử cuối cùng trước.
Dave Abrahams

1
@DaveAbrahams: Giữa chỉ foldlfoldrvà bỏ qua tính nghiêm minh và tối ưu hóa, phương tiện đầu tiên "ngoài cùng", không phải "thâm tâm". Đây là lý do tại sao foldrcó thể xử lý danh sách vô hạn và foldlkhông thể - nếp gấp bên phải trước tiên áp dụng fcho phần tử danh sách đầu tiên và kết quả (không được đánh giá) của việc gấp đuôi, trong khi nếp gấp bên trái phải duyệt qua toàn bộ danh sách để đánh giá ứng dụng ngoài cùng của f.
CA McCann

1
Tôi chỉ tự hỏi nếu có bất kỳ trường hợp nào mà nếp gấp sẽ được ưu tiên hơn màn hình gập ', bạn có nghĩ rằng có một trường hợp không?
kazuoua

1
@kazuoua nơi sự lười biếng là điều cần thiết, ví dụ 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..]

Vân vân.

Theo trực giác, foldlluôn ở bên "ngoài" hoặc bên trái nên nó được mở rộng trước. Nội dung quảng cáo.


10

Bạn có thể thấy trong tài liệu của Haskell ở đây rằng nếp gấp là đệ quy đuôi và sẽ không bao giờ kết thúc nếu được chuyển qua một danh sách vô hạn, vì nó tự gọi tham số tiếp theo trước khi trả về giá trị ...


0

Tôi không biết Haskell, nhưng trong Scheme, fold-rightsẽ luôn 'hành động' trước phần tử cuối cùng của danh sách. Do đó, sẽ không hoạt động đối với danh sách theo chu kỳ (giống với danh sách vô hạn).

Tôi không chắc liệu fold-rightcó thể được viết đệ quy đuôi hay không, nhưng đối với bất kỳ danh sách tuần hoàn nào, bạn sẽ gặp phải lỗi tràn ngăn xếp. fold-leftOTOH thường được triển khai với đệ quy đuôi, và sẽ chỉ bị mắc kẹt trong một vòng lặp vô hạn, nếu không kết thúc sớm.


3
Nó khác ở Haskell vì sự lười biếng.
Lifu Huang
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.