Clojure: kurangi vs. terapkan

126

Saya memahami perbedaan konseptual antara reducedan apply:

(reduce + (list 1 2 3 4 5))
; translates to: (+ (+ (+ (+ 1 2) 3) 4) 5)

(apply + (list 1 2 3 4 5))
; translates to: (+ 1 2 3 4 5)

Namun, mana yang lebih clojure idiomatik? Apakah itu membuat banyak perbedaan di satu sisi atau yang lain? Dari pengujian kinerja saya (terbatas), sepertinya reducesedikit lebih cepat.

dbyrne
sumber

Jawaban:

125

reducedan applytentu saja hanya setara (dalam hal hasil akhir dikembalikan) untuk fungsi asosiatif yang perlu melihat semua argumen mereka dalam kasus variabel-arity. Ketika mereka setara hasil-bijaksana, saya akan mengatakan itu applyselalu sangat idiomatis, sementara reduceitu setara - dan mungkin memangkas sebagian dari sekejap mata - dalam banyak kasus umum. Berikut ini adalah alasan saya untuk percaya ini.

+itu sendiri diimplementasikan dalam hal reducekasus variabel-arity (lebih dari 2 argumen). Memang, ini sepertinya cara "default" yang sangat masuk akal untuk dilakukan untuk variabel-arity, fungsi asosiatif: reducememiliki potensi untuk melakukan beberapa optimisasi untuk mempercepat segalanya - mungkin melalui sesuatu seperti internal-reduce, 1,2 kebaruan yang baru saja dinonaktifkan di master, tetapi mudah-mudahan akan diperkenalkan kembali di masa depan - yang konyol untuk mereplikasi di setiap fungsi yang mungkin mendapat manfaat dari mereka dalam kasus vararg. Dalam kasus umum seperti itu, applyhanya akan menambah sedikit overhead. (Perhatikan bahwa tidak ada yang perlu dikhawatirkan.)

Di sisi lain, fungsi yang kompleks mungkin mengambil keuntungan dari beberapa peluang optimisasi yang tidak cukup umum untuk dibangun reduce; maka applyakan membiarkan Anda mengambil keuntungan dari mereka sementara reducemungkin sebenarnya memperlambat Anda. Contoh yang baik dari skenario terakhir yang terjadi dalam praktik disediakan oleh str: ia menggunakan StringBuilderinternal dan akan mendapat manfaat secara signifikan dari penggunaan applydaripada reduce.

Jadi, saya akan mengatakan gunakan applysaat ragu; dan jika Anda tahu bahwa itu tidak membuat Anda kesal reduce(dan bahwa ini tidak mungkin berubah segera), jangan ragu reduceuntuk mencukur habisnya biaya tambahan yang tidak perlu jika Anda menginginkannya.

Michał Marczyk
sumber
Jawaban yang bagus Di samping catatan, mengapa tidak menyertakan sumfungsi bawaan seperti di haskell? Sepertinya operasi yang sangat umum.
dbyrne
17
Terima kasih, senang mendengarnya! Re: sumSaya akan mengatakan bahwa Clojure memiliki fungsi ini, namanya +dan Anda dapat menggunakannya apply. :-) Secara serius, saya pikir dalam Lisp, secara umum, jika fungsi variadik disediakan, biasanya tidak disertai oleh pembungkus yang beroperasi pada koleksi - itulah yang Anda gunakan applyuntuk (atau reduce, jika Anda tahu itu lebih masuk akal).
Michał Marczyk
6
Lucu, saran saya adalah kebalikannya: reduceketika ragu, applyketika Anda tahu pasti ada optimasi. reduceKontrak lebih tepat dan dengan demikian lebih rentan terhadap optimasi umum. applylebih kabur dan dengan demikian hanya dapat dioptimalkan berdasarkan kasus per kasus. strdan concatmerupakan dua pengecualian umum.
cgrand
1
@ grand Sebuah pengulangan ulang dari alasan saya mungkin kira-kira untuk fungsi di mana reducedan applysetara dalam hal hasil, saya berharap penulis fungsi tersebut tahu bagaimana cara terbaik untuk mengoptimalkan kelebihan variadic mereka dan hanya menerapkannya dalam hal reducejika memang itulah yang paling masuk akal (opsi untuk melakukannya tentu selalu tersedia dan menjadikannya default yang masuk akal). Saya melihat dari mana Anda berasal, reducejelas merupakan pusat kisah kinerja Clojure (dan semakin meningkat), sangat dioptimalkan dan sangat jelas.
Michał Marczyk
51

Untuk pemula yang melihat jawaban ini,
berhati-hatilah, mereka tidak sama:

(apply hash-map [:a 5 :b 6])
;= {:a 5, :b 6}
(reduce hash-map [:a 5 :b 6])
;= {{{:a 5} :b} 6}
David Rz Ayala
sumber
21

Pendapat bervariasi- Di dunia Lisp yang lebih besar, reducepasti dianggap lebih idiomatis. Pertama, ada masalah variadik yang sudah dibahas. Juga, beberapa kompiler Lisp Umum sebenarnya akan gagal ketika applyditerapkan terhadap daftar yang sangat panjang karena cara mereka menangani daftar argumen.

Di antara Clojurists di lingkaran saya, menggunakan applydalam kasus ini tampaknya lebih umum. Saya merasa lebih mudah untuk grok dan lebih suka juga.

drcode
sumber
19

Itu tidak membuat perbedaan dalam kasus ini, karena + adalah kasus khusus yang dapat diterapkan pada sejumlah argumen. Reduce adalah cara untuk menerapkan fungsi yang mengharapkan sejumlah argumen (2) ke daftar argumen yang panjang dan sewenang-wenang.

G__
sumber
9

Saya biasanya menemukan diri saya lebih suka mengurangi ketika bertindak pada segala jenis koleksi - berkinerja baik, dan merupakan fungsi yang cukup berguna secara umum.

Alasan utama yang akan saya gunakan berlaku adalah jika parameter berarti hal yang berbeda di posisi yang berbeda, atau jika Anda memiliki beberapa parameter awal tetapi ingin mendapatkan sisanya dari koleksi, mis.

(apply + 1 2 other-number-list)
mikera
sumber
9

Dalam kasus khusus ini saya lebih suka reducekarena lebih mudah dibaca : ketika saya membaca

(reduce + some-numbers)

Saya segera tahu bahwa Anda mengubah urutan menjadi nilai.

Dengan applysaya harus mempertimbangkan fungsi mana yang sedang diterapkan: "ah, itu +fungsinya, jadi saya mendapatkan ... satu nomor". Sedikit kurang lugas.

Mascip
sumber
7

Saat menggunakan fungsi sederhana seperti +, tidak masalah yang mana yang Anda gunakan.

Secara umum, idenya adalah reduceoperasi yang terakumulasi. Anda menyajikan nilai akumulasi saat ini dan satu nilai baru ke fungsi akumulasi Anda. Hasil dari fungsi tersebut adalah nilai kumulatif untuk iterasi berikutnya. Jadi, iterasi Anda terlihat seperti:

cum-val[i+1] = F( cum-val[i], input-val[i] )    ; please forgive the java-like syntax!

Untuk diterapkan, idenya adalah bahwa Anda mencoba memanggil fungsi yang mengharapkan sejumlah argumen skalar, tetapi mereka saat ini dalam koleksi dan perlu ditarik keluar. Jadi, alih-alih mengatakan:

vals = [ val1 val2 val3 ]
(some-fn (vals 0) (vals 1) (vals 2))

kita bisa bilang:

(apply some-fn vals)

dan itu dikonversi menjadi setara dengan:

(some-fn val1 val2 val3)

Jadi, menggunakan "berlaku" seperti "menghapus tanda kurung" di sekitar urutan.

Alan Thompson
sumber
4

Agak terlambat pada topik tetapi saya melakukan percobaan sederhana setelah membaca contoh ini. Ini adalah hasil dari balasan saya, saya tidak bisa menyimpulkan apa pun dari respons, tetapi sepertinya ada semacam tendangan caching di antara pengurangan dan penerapan.

user=> (time (reduce + (range 1e3)))
"Elapsed time: 5.543 msecs"
499500
user=> (time (apply + (range 1e3))) 
"Elapsed time: 5.263 msecs"
499500
user=> (time (apply + (range 1e4)))
"Elapsed time: 19.721 msecs"
49995000
user=> (time (reduce + (range 1e4)))
"Elapsed time: 1.409 msecs"
49995000
user=> (time (reduce + (range 1e5)))
"Elapsed time: 17.524 msecs"
4999950000
user=> (time (apply + (range 1e5)))
"Elapsed time: 11.548 msecs"
4999950000

Melihat kode sumber clojure mengurangi rekursi yang cukup bersih dengan internal-reduction, tidak menemukan apa pun pada penerapan yang berlaku sekalipun. Implementasi Clojure dari + untuk menerapkan pengurangan invoke secara internal, yang di-cache dengan repl, yang tampaknya menjelaskan panggilan ke-4. Bisakah seseorang mengklarifikasi apa yang sebenarnya terjadi di sini?

rohit
sumber
Saya tahu saya akan lebih suka mengurangi kapan saja saya bisa :)
rohit
2
Anda tidak harus memasukkan rangepanggilan ke dalam timeformulir. Letakkan di luar untuk menghilangkan gangguan konstruksi urutan. Dalam kasus saya, reducesecara konsisten mengungguli apply.
Davyzhu
3

Keindahan penerapan diberikan fungsi (+ dalam hal ini) dapat diterapkan pada daftar argumen yang dibentuk oleh argumen intervensi sebelumnya dengan koleksi akhir. Reduce adalah abstraksi untuk memproses item koleksi yang menerapkan fungsi untuk masing-masing dan tidak bekerja dengan kasus args variabel.

(apply + 1 2 3 [3 4])
=> 13
(reduce + 1 2 3 [3 4])
ArityException Wrong number of args (5) passed to: core/reduce  clojure.lang.AFn.throwArity (AFn.java:429)
Ira
sumber