foldl versus foldr behaviour dengan daftar tak terbatas

124

Kode untuk fungsi myAny dalam pertanyaan ini menggunakan foldr. Ini berhenti memproses daftar tak terbatas ketika predikat terpenuhi.

Saya menulis ulang menggunakan foldl:

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

(Perhatikan bahwa argumen ke fungsi langkah dibalik dengan benar.)

Namun, itu tidak lagi berhenti memproses daftar tak terbatas.

Saya mencoba melacak eksekusi fungsi seperti dalam jawaban 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

Namun, ini bukan cara fungsi tersebut berperilaku. Bagaimana ini salah?

titaniumdecoy
sumber

Jawaban:

231

Bagaimana fold perbedaannya tampaknya sering menjadi sumber kebingungan, jadi inilah gambaran umum yang lebih umum:

Pertimbangkan melipat daftar nilai n [x1, x2, x3, x4 ... xn ]dengan beberapa fungsif dan benih z.

foldl adalah:

  • Asosiatif kiri :f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
  • Ekor rekursif : Ini mengulangi daftar, menghasilkan nilai sesudahnya
  • Malas : Tidak ada yang dievaluasi sampai hasilnya dibutuhkan
  • Mundur :foldl (flip (:)) [] membalikkan daftar.

foldr adalah:

  • Asosiatif yang tepat :f x1 (f x2 (f x3 (f x4 ... (f xn z) ... )))
  • Rekursif menjadi argumen : Setiap iterasi diterapkan fke nilai berikutnya dan hasil melipat sisa daftar.
  • Malas : Tidak ada yang dievaluasi sampai hasilnya dibutuhkan
  • Meneruskan :foldr (:) [] mengembalikan daftar yang tidak berubah.

Ada titik sedikit halus di sini bahwa perjalanan orang sampai kadang-kadang: Karena foldladalah mundur setiap aplikasi dari fditambahkan ke luar dari hasil; dan karena malas , tidak ada yang dievaluasi hingga hasilnya diperlukan. Ini berarti bahwa untuk menghitung bagian mana pun dari hasil, Haskell pertama-tama melakukan iterasi melalui seluruh daftar yang membuat ekspresi aplikasi fungsi bersarang, lalu mengevaluasi fungsi terluar , mengevaluasi argumennya sesuai kebutuhan. Jika fselalu menggunakan argumen pertamanya, ini berarti Haskell harus mengulang hingga ke istilah paling dalam, lalu bekerja menghitung mundur setiap aplikasi f.

Ini jelas jauh dari rekursi-ekor efisien yang paling dikenal dan disukai oleh sebagian besar programmer fungsional!

Faktanya, meskipun foldlsecara teknis rekursif-ekor, karena seluruh ekspresi hasil dibuat sebelum mengevaluasi apa pun,foldl dapat menyebabkan tumpukan melimpah!

Di sisi lain, pertimbangkan foldr. Ini juga malas, tetapi karena berjalan ke depan , setiap aplikasi fditambahkan ke bagian dalam hasil. Jadi, untuk menghitung hasilnya, Haskell membuat aplikasi fungsi tunggal , yang argumen kedua adalah daftar lipat lainnya. Jika fmalas dalam argumen keduanya - konstruktor data, misalnya - hasilnya akan malas secara bertahap , dengan setiap langkah lipatan dihitung hanya ketika beberapa bagian dari hasil yang membutuhkannya dievaluasi.

Jadi kita dapat melihat mengapa foldrkadang - kadang bekerja pada daftar tak terbatas jika foldltidak: Yang pertama dapat dengan malas mengonversi daftar tak hingga menjadi struktur data tak terbatas malas lainnya, sedangkan yang terakhir harus memeriksa seluruh daftar untuk menghasilkan bagian mana pun dari hasil. Di sisi lain, foldrdengan fungsi yang membutuhkan kedua argumen dengan segera, seperti (+), berfungsi (atau lebih tepatnya, tidak berfungsi) seperti foldl, membangun ekspresi yang sangat besar sebelum mengevaluasinya.

Jadi dua hal penting yang perlu diperhatikan adalah ini:

  • foldr dapat mengubah satu struktur data rekursif malas menjadi yang lain.
  • Jika tidak, lipatan malas akan macet dengan tumpukan melimpah pada daftar besar atau tak terbatas.

Anda mungkin telah memperhatikan bahwa sepertinya foldrbisa melakukan apa saja foldl, dan lebih banyak lagi. Ini benar! Faktanya, foldl hampir tidak berguna!

Tetapi bagaimana jika kita ingin menghasilkan hasil yang tidak malas dengan melipat daftar besar (tetapi tidak tak terbatas)? Untuk ini, kami menginginkan lipatan yang ketat , yang dengan cermat disediakan oleh pustaka standar :

foldl' adalah:

  • Asosiatif kiri :f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
  • Ekor rekursif : Ini mengulangi daftar, menghasilkan nilai sesudahnya
  • Ketat : Setiap aplikasi fungsi dievaluasi sepanjang proses
  • Mundur :foldl' (flip (:)) [] membalikkan daftar.

Karena foldl'ini ketat , untuk menghitung hasil Haskell akan mengevaluasi f pada setiap langkah, alih-alih membiarkan argumen kiri menumpuk besar, ekspresi unevaluated. Ini memberi kita rekursi ekor biasa dan efisien yang kita inginkan! Dengan kata lain:

  • foldl' dapat melipat daftar besar secara efisien.
  • foldl' akan bertahan dalam putaran tak terbatas (tidak menyebabkan tumpukan melimpah) pada daftar tak terbatas.

Wiki Haskell memiliki halaman yang membahas hal ini juga.

CA McCann
sumber
6
Saya datang ke sini karena saya penasaran mengapa foldrlebih baik daripada foldldi Haskell , sedangkan sebaliknya di Erlang (yang saya pelajari sebelum Haskell ). Karena Erlang tidak malas dan fungsinya tidak kari , maka foldldi Erlang berperilaku seperti di foldl'atas. Ini jawaban yang bagus! Kerja bagus dan terima kasih!
Siu Ching Pong -Asuka Kenji-
7
Ini sebagian besar adalah penjelasan yang bagus, tetapi saya menemukan deskripsi foldlsebagai "terbelakang" dan foldrsebagai "maju" bermasalah. Ini sebagian karena flipditerapkan (:)pada ilustrasi mengapa lipatan terbalik. Reaksi alaminya adalah, "tentu saja itu mundur: Anda membuat flipdaftar!" Ini juga aneh untuk melihat yang disebut "mundur" karena foldlberlaku funtuk elemen daftar pertama pertama (paling dalam) dalam evaluasi lengkap. Itu adalah foldr"berjalan mundur", yang diterapkan fke elemen terakhir terlebih dahulu.
Dave Abrahams
1
@DaveAbrahams: Antara adil foldldan foldrdan mengabaikan ketelitian dan pengoptimalan, pertama berarti "paling luar", bukan "paling dalam". Inilah sebabnya mengapa foldrdapat memproses daftar tak terbatas dan foldltidak bisa - lipatan kanan pertama-tama berlaku funtuk elemen daftar pertama dan hasil (tidak dievaluasi) melipat ekor, sedangkan lipatan kiri harus melintasi seluruh daftar untuk mengevaluasi penerapan terluar f.
CA McCann
1
Saya hanya ingin tahu apakah ada contoh di mana lipatan lebih disukai daripada lipatan, menurut Anda ada satu?
kazuoua
1
@kazuoua di mana kemalasan itu penting, mis 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..]

dll.

Secara intuitif, foldlselalu berada di "luar" atau di "kiri" sehingga lebih dulu diperluas. Ad infinitum.

Artelius
sumber
10

Anda dapat melihat dalam dokumentasi Haskell di sini bahwa foldl adalah rekursif-ekor dan tidak akan pernah berakhir jika diteruskan daftar tak terbatas, karena ia memanggil dirinya sendiri pada parameter berikutnya sebelum mengembalikan nilai ...

Romain
sumber
0

Saya tidak tahu Haskell, tetapi dalam Skema, fold-rightakan selalu 'bertindak' pada elemen terakhir dari daftar terlebih dahulu. Dengan demikian, is tidak akan berfungsi untuk daftar siklik (yang sama dengan daftar tak hingga).

Saya tidak yakin apakah fold-rightdapat ditulis rekursif-ekor, tetapi untuk daftar siklik apa pun Anda harus mendapatkan stack overflow. fold-leftOTOH biasanya diimplementasikan dengan rekursi ekor, dan hanya akan terjebak dalam loop tak terbatas, jika tidak menghentikannya lebih awal.

leppie
sumber
3
Ini berbeda di Haskell karena kemalasan.
Lifu Huang