Clojure: azaltın ve uygulayın


126

Ben arasındaki kavramsal farkı anlamak reduceve 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)

Bununla birlikte, hangisi daha deyimsel taklittir? Öyle ya da böyle çok fark yaratıyor mu? (Sınırlı) performans testime göre, reducebiraz daha hızlı görünüyor .

Yanıtlar:


125

reduceve applyelbette, değişken-arity durumunda tüm argümanlarını görmesi gereken ilişkisel işlevler için yalnızca eşdeğerdir (döndürülen nihai sonuç açısından). Sonuç açısından eşdeğer olduklarında, bunun applyher zaman mükemmel bir deyimsel olduğunu söyleyebilirim , ancak reduceeşdeğerdir - ve birçok yaygın durumda göz açıp kapayıncaya kadar bir miktar tıraş olabilir. Aşağıda buna inanmak için mantığım var.

+reducedeğişken-arity durumu (2'den fazla argüman) açısından kendisi uygulanmaktadır . Aslında, bu, herhangi bir değişken ariteye, ilişkisel işlev için son derece mantıklı bir "varsayılan" yol gibi görünüyor: reduceişleri hızlandırmak için bazı optimizasyonları gerçekleştirme potansiyeline sahiptir - belki de internal-reduce, yakın zamanda ustada devre dışı bırakılan 1.2 yenilik gibi bir şey olabilir , ancak umarım gelecekte yeniden tanıtılabilir - ki vararg durumunda bunlardan fayda sağlayabilecek her işlevde kopyalamak aptalca olur. Bu tür yaygın durumlarda, applybiraz ek yük getirecektir. (Gerçekten endişelenecek bir şey olmadığını unutmayın.)

Öte yandan, karmaşık bir işlev, içine yerleştirilmek için yeterince genel olmayan bazı optimizasyon fırsatlarından yararlanabilir reduce; o applyzaman reducegerçekten sizi yavaşlatabilirken bunlardan yararlanmanıza izin verir . Uygulamada ortaya çıkan ikinci senaryonun iyi bir örneği şu şekilde sağlanır str: StringBuilderdahili olarak kullanır ve applyyerine kullanımından önemli ölçüde faydalanacaktır reduce.

Yani, applyşüphe duyduğunuzda kullanın derim ; ve bunun size bir şey satın almayacağını reduce(ve bunun çok yakında değişme ihtimalinin düşük olduğunu) reducebilirseniz, dilerseniz o küçültücü gereksiz ek yükü tıraş etmekten çekinmeyin.


Mükemmel cevap. Bir yan not olarak, neden sumhaskell'deki gibi yerleşik bir işlevi dahil etmiyorsunuz ? Oldukça yaygın bir operasyon gibi görünüyor.
dbyrne

17
Teşekkürler, bunu duyduğuma sevindim! Re: sumClojure'un bu işleve sahip olduğunu söyleyebilirim, buna denir +ve onu kullanabilirsiniz apply. :-) Cidden konuşursak, Lisp'te genel olarak, eğer değişken bir işlev sağlanmışsa, genellikle koleksiyonlar üzerinde çalışan bir sarmalayıcıya eşlik etmez - bunun applyiçin kullanırsınız (ya da reducebunun daha mantıklı olduğunu biliyorsanız).
Michal MARCZYK

6
Komik, tavsiyem tam tersi: reduceşüphe applyduyduğunuzda , bir optimizasyon olduğundan emin olduğunuzda. reducesözleşmesinin daha kesin olması ve dolayısıyla genel optimizasyona daha yatkın olması. applydaha belirsizdir ve bu nedenle yalnızca duruma göre optimize edilebilir. strve concatiki yaygın istisna vardır.
cgrand

1
@cgrand benim mantığı bir Yeniden ifadelendirmenin fonksiyonları için kabaca olabilir reduceve applysonuçları açısından eşdeğerdir, onların variadic aşırı optimize etmek nasıl en iyi bilir ve sadece terimlerin bunu uygulamak için söz konusu fonksiyonun yazarı beklediğiniz reduceeğer gerçekten de en mantıklı olan şey budur (bunu yapma seçeneği kesinlikle her zaman mevcuttur ve fazlasıyla mantıklı bir varsayılan oluşturur). Nereden geldiğinizi anlıyorum, yine de reduce, kesinlikle Clojure'un performans öyküsünün merkezinde (ve giderek daha fazla), çok yüksek düzeyde optimize edilmiş ve çok net bir şekilde belirlenmiş.
Michał Marczyk

51

Bu cevaba bakan yeni başlayanlar
için dikkatli olun, bunlar aynı değildir:

(apply hash-map [:a 5 :b 6])
;= {:a 5, :b 6}
(reduce hash-map [:a 5 :b 6])
;= {{{:a 5} :b} 6}

21

Görüşler değişir - Daha büyük Lisp dünyasında, reducekesinlikle daha deyimsel olarak kabul edilir. Birincisi, zaten tartışılan çeşitli konular var. Ayrıca, bazı Common Lisp derleyicileri, applyargüman listelerini nasıl ele aldıklarından dolayı çok uzun listelere uygulandıklarında gerçekten başarısız olurlar.

Çevremdeki Clojuristler arasında applybu durumda kullanmak daha yaygın görünüyor. Sallamayı ve onu tercih etmeyi daha kolay buluyorum.


19

Bu durumda bir fark yaratmaz, çünkü + herhangi bir sayıda argümana uygulanabilen özel bir durumdur. Azaltma, sabit sayıda argüman (2) bekleyen bir işlevi rastgele uzun bir argüman listesine uygulamanın bir yoludur.


9

Normalde kendimi herhangi bir koleksiyon türünde hareket ederken indirgemeyi tercih ederken buluyorum - iyi performans gösteriyor ve genel olarak oldukça kullanışlı bir işlev.

Uygulamayı kullanmamın ana nedeni, parametrelerin farklı konumlarda farklı şeyler ifade etmesi veya birkaç başlangıç ​​parametreniz varsa ancak geri kalanını bir koleksiyondan almak istiyorsanız, örn.

(apply + 1 2 other-number-list)

9

Bu özel durumda tercih ederim reduceçünkü daha okunaklı : okuduğumda

(reduce + some-numbers)

Bir diziyi bir değere dönüştürdüğünüzü hemen anlıyorum.

Bununla birlikte apply, hangi işlevin uygulandığını düşünmem gerekiyor: "ah, bu +işlev, yani ... tek bir sayı alıyorum". Biraz daha basit.


7

+ Gibi basit bir işlevi kullanırken, hangisini kullandığınız önemli değildir.

Genel olarak fikir, reducebiriken bir işlemdir. Mevcut biriktirme değerini ve bir yeni değeri biriktirme işlevinize sunarsınız İşlevin sonucu, sonraki yineleme için kümülatif değerdir. Yani yinelemeleriniz şöyle görünür:

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

Başvuru için fikir, bir dizi skaler bağımsız değişken bekleyen bir işlevi çağırmaya çalıştığınız, ancak şu anda bir koleksiyonda oldukları ve çıkarılmaları gerektiğidir. Yani demek yerine:

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

söyleyebiliriz:

(apply some-fn vals)

ve şuna eşdeğer olacak şekilde dönüştürülür:

(some-fn val1 val2 val3)

Dolayısıyla, "uygula" yı kullanmak, dizinin etrafındaki "parantezleri kaldırmak" gibidir.


4

Konuyla biraz geç kaldım ama bu örneği okuduktan sonra basit bir deney yaptım. İşte repl'imin sonucu, yanıttan hiçbir şey çıkaramıyorum, ancak azaltma ve uygulama arasında bir tür önbellekleme teklemesi var gibi görünüyor.

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

Clojure kaynak koduna bakıldığında iç azaltma ile oldukça temiz özyinelemesini azaltın, ancak application uygulamasının uygulanmasında hiçbir şey bulamadı. + For application'ın Clojure uygulaması dahili olarak, repl tarafından önbelleğe alınan ve 4. çağrıyı açıklıyor gibi görünen azaltmayı çağırır. Birisi burada gerçekten ne olduğunu açıklayabilir mi?



2
Sen koymak gerekir rangeiçeride çağrıyı timeformu. Sıralı yapının girişimini ortadan kaldırmak için dışarıya koyun. Benim durumumda, reducesürekli olarak daha iyi performans gösteriyor apply.
Davyzhu

3

Uygulamanın güzelliği fonksiyonu verilir (bu durumda +), bir biten koleksiyon ile önceden beklemede olan araya giren argümanlarla oluşturulan argüman listesine uygulanabilir. Azaltma, her biri için işlevi uygulayan koleksiyon öğelerini işlemek için bir soyutlamadır ve değişken args durumuyla çalışmaz.

(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)
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.