Implikasi foldr vs. foldl (atau foldl ')

155

Pertama, Real World Haskell , yang saya baca, mengatakan untuk tidak pernah menggunakan foldldan sebaliknya menggunakannya foldl'. Jadi saya percaya itu.

Tapi aku kabur pada saat menggunakan foldrvs foldl'. Meskipun saya dapat melihat struktur bagaimana mereka bekerja secara berbeda diletakkan di depan saya, saya terlalu bodoh untuk mengerti kapan "mana yang lebih baik." Saya kira menurut saya seharusnya tidak masalah yang digunakan, karena mereka berdua menghasilkan jawaban yang sama (bukan?). Sebenarnya, pengalaman saya sebelumnya dengan konstruksi ini adalah dari versi Ruby injectdan Clojure reduce, yang sepertinya tidak memiliki versi "kiri" dan "kanan". (Pertanyaan sampingan: versi apa yang mereka gunakan?)

Wawasan apa pun yang dapat membantu orang yang memiliki kecerdasan seperti saya akan sangat dihargai!

J Cooper
sumber

Jawaban:

174

Rekursi di foldr f x ysmana ys = [y1,y2,...,yk]terlihat seperti

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

sedangkan rekursi untuk foldl f x ysterlihat seperti

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

Perbedaan penting di sini adalah bahwa jika hasil f x ydapat dihitung hanya dengan menggunakan nilai x, maka foldrtidak perlu memeriksa seluruh daftar. Sebagai contoh

foldr (&&) False (repeat False)

mengembalikan Falsesedangkan

foldl (&&) False (repeat False)

tidak pernah berakhir. (Catatan: repeat Falsemembuat daftar tak terbatas di mana setiap elemen berada False.)

Di sisi lain, foldl'ekornya rekursif dan ketat. Jika Anda tahu bahwa Anda harus melintasi seluruh daftar tidak peduli apa (misalnya, menjumlahkan angka-angka dalam daftar), maka foldl'lebih banyak ruang- (dan mungkin waktu-) lebih efisien daripada foldr.

Chris Conway
sumber
2
Dalam foldr ia mengevaluasi sebagai th y1 thunk, sehingga mengembalikan False, namun dalam foldl, f tidak dapat mengetahui salah satu dari parameter itu. terlalu besar. foldl 'dapat mengurangi thunk segera di sepanjang eksekusi.
Sawyer
25
Untuk menghindari kebingungan, perhatikan bahwa tanda kurung tidak menunjukkan urutan evaluasi yang sebenarnya. Karena Haskell malas, ekspresi terluar akan dievaluasi terlebih dahulu.
Lii
Greate answer. Saya ingin menambahkan bahwa jika Anda ingin flip yang dapat berhenti sebagian jalan melalui daftar, Anda harus menggunakan foldr; kecuali saya salah, lipatan kiri tidak bisa dihentikan. (Anda mengisyaratkan ini ketika Anda mengatakan "jika Anda tahu ... Anda akan ... melintasi seluruh daftar"). Juga, kesalahan ketik "hanya menggunakan nilai" harus diubah menjadi "hanya menggunakan nilai". Yaitu menghapus kata "on". (Stackoverflow tidak akan membiarkan saya mengirimkan perubahan 2 karakter!).
Lqueryvg
@Queryvg dua cara untuk menghentikan lipatan kiri: 1. kode dengan lipatan kanan (lihat fodlWhile); 2. mengubahnya menjadi pemindaian kiri ( scanl) dan hentikan dengan last . takeWhile patau serupa. Uh, dan 3. gunakan mapAccumL. :)
Will Ness
1
@Desty karena ia menghasilkan bagian baru dari hasil keseluruhannya pada setiap langkah - tidak seperti foldl, yang mengumpulkan hasil keseluruhannya dan memproduksinya hanya setelah semua pekerjaan selesai dan tidak ada lagi langkah untuk dilakukan. Jadi misalnya foldl (flip (:)) [] [1..3] == [3,2,1], jadi scanl (flip(:)) [] [1..] = [[],[1],[2,1],[3,2,1],...]... TKI, foldl f z xs = last (scanl f z xs)dan daftar tak terbatas tidak memiliki elemen terakhir (yang, dalam contoh di atas, itu sendiri akan menjadi daftar tak terbatas, dari INF ke 1).
Will Ness
57

foldr terlihat seperti ini:

Visualisasi lipat kanan

foldl terlihat seperti ini:

Visualisasi lipatan kiri

Konteks: Lipat di wiki Haskell

Greg Bacon
sumber
33

Semantik mereka berbeda sehingga Anda tidak bisa hanya bertukar foldldan foldr. Yang satu melipat elemen dari kiri, yang lain dari kanan. Dengan begitu, operator diterapkan dalam urutan yang berbeda. Ini penting untuk semua operasi non-asosiatif, seperti pengurangan.

Haskell.org memiliki artikel yang menarik tentang masalah ini.

Konrad Rudolph
sumber
Semantik mereka hanya berbeda secara sepele, dalam praktiknya tidak ada artinya: Urutan argumen fungsi yang digunakan. Jadi antarmuka-bijaksana mereka masih dianggap bisa ditukar. Perbedaan sebenarnya adalah, sepertinya, hanya optimasi / implementasi.
Evi1M4chine
2
@ Evi1M4chine Tidak ada perbedaan yang sepele. Sebaliknya, mereka substansial (dan, ya, bermakna dalam praktik). Bahkan, jika saya menulis jawaban ini hari ini akan lebih menekankan perbedaan ini.
Konrad Rudolph
23

Singkatnya, foldrlebih baik ketika fungsi akumulator malas pada argumen keduanya. Baca lebih lanjut di Stack Overflow Haskell wiki (pun intended).

Mattiast
sumber
18

Alasannya foldl'lebih disukai foldluntuk 99% dari semua penggunaan adalah dapat digunakan dalam ruang konstan untuk sebagian besar penggunaan.

Ambil fungsinya sum = foldl['] (+) 0. Ketika foldl'digunakan, jumlahnya langsung dihitung, jadi menerapkan sumke daftar tak terbatas hanya akan berjalan selamanya, dan kemungkinan besar di ruang konstan (jika Anda menggunakan hal-hal seperti Ints, Doubles, Floats. IntegerS akan menggunakan lebih dari ruang konstan jika nomor menjadi lebih besar dari maxBound :: Int).

Dengan foldl, thunk dibangun (seperti resep bagaimana mendapatkan jawabannya, yang dapat dievaluasi nanti, daripada menyimpan jawabannya). Pemukul ini bisa menghabiskan banyak ruang, dan dalam hal ini, jauh lebih baik untuk mengevaluasi ekspresi daripada menyimpan pemogokan (mengarah ke tumpukan meluap ... dan membawa Anda ke ... oh tidak apa-apa)

Semoga itu bisa membantu.

Axman6
sumber
1
Pengecualian besar adalah jika fungsi yang diteruskan ke foldlmelakukan apa-apa selain menerapkan konstruktor ke satu atau lebih argumennya.
dfeuer
Apakah ada pola umum kapan foldlsebenarnya pilihan terbaik? (Seperti daftar yang tidak terbatas kapan foldrpilihan yang salah, dari
segi
@ Evi1M4chine tidak yakin apa yang Anda maksud dengan foldrmenjadi pilihan yang salah untuk daftar tak terbatas. Bahkan, Anda tidak boleh menggunakan foldlatau foldl'untuk daftar yang tak terbatas. Lihat wiki Haskell di stack overflows
KevinOrr
14

By the way, Ruby injectdan Clojure ini reduceadalah foldl(atau foldl1, tergantung pada versi yang Anda gunakan). Biasanya, ketika hanya ada satu bentuk dalam bahasa, itu adalah lipatan kiri, termasuk Python reduce, Perl List::Util::reduce, C ++ accumulate, C # Aggregate, Smalltalk inject:into:, PHP array_reduce, Mathematica Fold, dll. Standar umum Lisp reduceuntuk lipatan kiri tetapi ada opsi untuk lipatan kanan.

berita baru
sumber
2
Komentar ini bermanfaat tetapi saya sangat menghargai sumbernya.
titaniumdecoy
Common Lisp's reducetidak malas, jadi itu foldl'dan banyak pertimbangan di sini tidak berlaku.
MicroVirus
Saya pikir maksud Anda foldl', karena mereka adalah bahasa yang ketat, bukan? Kalau tidak, bukankah itu berarti semua versi itu menyebabkan stack overflow seperti yang foldlterjadi?
Evi1M4chine
6

Seperti yang ditunjukkan Konrad , semantik mereka berbeda. Mereka bahkan tidak memiliki tipe yang sama:

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

Sebagai contoh, daftar append operator (++) dapat diimplementasikan dengan foldras

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

sementara

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

akan memberi Anda kesalahan ketik.

Jonas
sumber
Tipe mereka sama, hanya berganti-ganti, yang tidak relevan. Antarmuka dan hasilnya sama, kecuali untuk masalah P / NP buruk (baca: daftar tak terbatas). ;) Optimalisasi karena implementasi adalah satu-satunya perbedaan dalam praktik, sejauh yang saya tahu.
Evi1M4chine
@ Evi1M4chine Ini tidak benar, lihat contoh ini: foldl subtract 0 [1, 2, 3, 4]evaluate to -10, sementara foldr subtract 0 [1, 2, 3, 4]evaluate to -2. foldlsebenarnya 0 - 1 - 2 - 3 - 4saat foldrini 4 - 3 - 2 - 1 - 0.
krapht
@krapht, foldr (-) 0 [1, 2, 3, 4]adalah -2dan foldl (-) 0 [1, 2, 3, 4]sedang -10. Di sisi lain, subtractadalah mundur dari apa yang Anda harapkan ( subtract 10 14adalah 4), sehingga foldr subtract 0 [1, 2, 3, 4]adalah -10dan foldl subtract 0 [1, 2, 3, 4]adalah 2(positif).
pianoJames