Clojure: reduzieren vs. anwenden

126

Ich verstehe den konzeptuellen Unterschied zwischen reduceund 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)

Welches ist jedoch idiomatischer Clojure? Macht es auf die eine oder andere Weise einen großen Unterschied? Nach meinen (eingeschränkten) Leistungstests scheint reducees etwas schneller zu sein.

dbyrne
quelle

Antworten:

125

reduceund applysind natürlich nur für assoziative Funktionen äquivalent (in Bezug auf das zurückgegebene Endergebnis), die alle ihre Argumente im Fall der variablen Arität sehen müssen. Wenn sie ergebnismäßig äquivalent sind, würde ich sagen, dass dies applyimmer vollkommen idiomatisch ist, während reducees in vielen Fällen äquivalent ist - und möglicherweise einen Bruchteil eines Augenblicks abschneidet. Was folgt, ist meine Begründung, dies zu glauben.

+wird selbst in Bezug reduceauf den Fall der variablen Arität implementiert (mehr als 2 Argumente). In der Tat scheint dies ein immens vernünftiger "Standard" -Weg für eine assoziative Funktion mit variabler Arität zu sein: Er reducehat das Potenzial, einige Optimierungen vorzunehmen, um die Dinge zu beschleunigen - vielleicht durch etwas wie internal-reduceeine 1.2-Neuheit, die kürzlich im Master deaktiviert wurde, aber hoffentlich in Zukunft wieder eingeführt zu werden - was es dumm wäre, in jeder Funktion zu replizieren, die im vararg-Fall von ihnen profitieren könnte. In solchen Fällen applywird nur ein wenig Overhead hinzugefügt. (Beachten Sie, dass Sie sich keine Sorgen machen müssen.)

Andererseits kann eine komplexe Funktion einige Optimierungsmöglichkeiten nutzen, die nicht allgemein genug sind, um eingebaut zu werden reduce. Dann können applySie diese nutzen, während Sie reducemöglicherweise tatsächlich langsamer werden. Ein gutes Beispiel für das letztere Szenario in der Praxis ist str: Es verwendet ein StringBuilderinternes und profitiert erheblich von der Verwendung von applyanstatt von reduce.

Also, ich würde sagen, verwenden Sie, applywenn Sie Zweifel haben; und wenn Sie zufällig wissen, dass es Ihnen nichts überkauft reduce(und dass sich dies wahrscheinlich nicht sehr bald ändern wird), können reduceSie diesen winzigen unnötigen Overhead verwenden, wenn Sie Lust dazu haben.

Michał Marczyk
quelle
Gute Antwort. sumNebenbei bemerkt, warum nicht eine eingebaute Funktion wie in Haskell einbinden? Scheint eine ziemlich häufige Operation zu sein.
dbyrne
17
Danke, freut mich das zu hören! Betreff: sumIch würde sagen, dass Clojure diese Funktion hat, sie heißt +und Sie können sie mit verwenden apply. :-) Im Ernst, ich denke, in Lisp wird eine variadische Funktion im Allgemeinen normalerweise nicht von einem Wrapper begleitet, der Sammlungen bearbeitet - dafür verwenden Sie sie apply(oder reduce, wenn Sie wissen, dass dies sinnvoller ist).
Michał Marczyk
6
Komisch, mein Rat ist das Gegenteil: reduceIm Zweifelsfall, applywenn Sie sicher wissen, dass es eine Optimierung gibt. reduceDer Vertrag ist präziser und daher anfälliger für allgemeine Optimierungen. applyist vager und kann daher nur von Fall zu Fall optimiert werden. strund concatsind die beiden vorherrschenden Ausnahmen.
Cgrand
1
@cgrand Eine Neuformulierung meiner Überlegungen könnte ungefähr so lauten, dass ich für Funktionen, bei denen reduceund applyin Bezug auf die Ergebnisse gleichwertig ist, erwarten würde, dass der Autor der betreffenden Funktion weiß, wie er ihre variadische Überlastung am besten optimieren und sie einfach in Bezug auf reduceif implementieren kann Das ist in der Tat am sinnvollsten (die Option dazu ist sicherlich immer verfügbar und sorgt für einen überaus vernünftigen Standard). Ich sehe jedoch, woher Sie kommen, reduceist definitiv von zentraler Bedeutung für Clojures Performance-Story (und zunehmend auch), sehr hoch optimiert und sehr klar spezifiziert.
Michał Marczyk
51

Seien Sie
vorsichtig, wenn Neulinge diese Antwort lesen: Sie sind nicht gleich:

(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
quelle
21

Die Meinungen sind unterschiedlich. In der Großwelt von Lisp reducewird dies definitiv als idiomatischer angesehen. Erstens gibt es die verschiedenen Themen, die bereits diskutiert wurden. Außerdem schlagen einige Common Lisp-Compiler tatsächlich fehl, wenn sie applyauf sehr lange Listen angewendet werden, da sie mit Argumentlisten umgehen.

Unter den Clojuristen in meinem Kreis applyscheint die Verwendung in diesem Fall jedoch häufiger zu sein. Ich finde es einfacher zu groken und bevorzuge es auch.

drcode
quelle
19

In diesem Fall macht es keinen Unterschied, da + ein Sonderfall ist, der auf eine beliebige Anzahl von Argumenten angewendet werden kann. Reduzieren ist eine Möglichkeit, eine Funktion, die eine feste Anzahl von Argumenten (2) erwartet, auf eine beliebig lange Liste von Argumenten anzuwenden.

G__
quelle
9

Normalerweise bevorzuge ich das Reduzieren, wenn ich auf irgendeine Art von Sammlung reagiere - es funktioniert gut und ist im Allgemeinen eine ziemlich nützliche Funktion.

Der Hauptgrund, den ich anwenden würde, ist, wenn die Parameter an verschiedenen Positionen unterschiedliche Bedeutungen haben oder wenn Sie einige Anfangsparameter haben, aber den Rest aus einer Sammlung erhalten möchten, z

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

In diesem speziellen Fall bevorzuge ich, reduceweil es besser lesbar ist : wenn ich lese

(reduce + some-numbers)

Ich weiß sofort, dass Sie eine Sequenz in einen Wert verwandeln.

Mit muss applyich überlegen, welche Funktion angewendet wird: "ah, es ist die +Funktion, also bekomme ich ... eine einzelne Zahl". Etwas weniger einfach.

Maskip
quelle
7

Wenn Sie eine einfache Funktion wie + verwenden, spielt es keine Rolle, welche Sie verwenden.

Im Allgemeinen ist die Idee, dass reducees sich um eine Akkumulationsoperation handelt. Sie präsentieren Ihrer Akkumulationsfunktion den aktuellen Akkumulationswert und einen neuen Wert. Das Ergebnis der Funktion ist der kumulative Wert für die nächste Iteration. Ihre Iterationen sehen also so aus:

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

Für die Anwendung besteht die Idee darin, dass Sie versuchen, eine Funktion aufzurufen, die eine Reihe von skalaren Argumenten erwartet, diese sich jedoch derzeit in einer Sammlung befinden und herausgezogen werden müssen. Also anstatt zu sagen:

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

Wir können sagen:

(apply some-fn vals)

und es wird konvertiert, um äquivalent zu sein:

(some-fn val1 val2 val3)

Die Verwendung von "Übernehmen" ist also wie "Entfernen der Klammern" um die Sequenz.

Alan Thompson
quelle
4

Etwas spät zum Thema, aber ich habe ein einfaches Experiment durchgeführt, nachdem ich dieses Beispiel gelesen hatte. Hier ist das Ergebnis meiner Antwort. Ich kann einfach nichts aus der Antwort ableiten, aber es scheint, dass zwischen Reduzieren und Anwenden eine Art Caching-Kick liegt.

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

Wenn man sich den Quellcode von clojure ansieht, reduziert man seine ziemlich saubere Rekursion mit internal-redu, hat aber bei der Implementierung von apply nichts gefunden. Clojure-Implementierung von + für Apply Intern Invoke Reduce, das von Repl zwischengespeichert wird, was den 4. Aufruf zu erklären scheint. Kann jemand klären, was hier wirklich passiert?

Rohit
quelle
Ich weiß, ich werde es vorziehen, zu reduzieren, wann immer ich kann :)
Rohit
2
Sie sollten den rangeAnruf nicht in das timeFormular einfügen. Stellen Sie es nach draußen, um die Störung der Sequenzkonstruktion zu beseitigen. In meinem Fall reduceübertrifft konsequent apply.
Davyzhu
3

Das Schöne an Apply ist, dass die Funktion (+ in diesem Fall) auf die Argumentliste angewendet werden kann, die aus bereits ausstehenden intervenierenden Argumenten mit einer Endsammlung besteht. Reduzieren ist eine Abstraktion zum Verarbeiten von Sammlungselementen, bei der die Funktion für jedes Element angewendet wird, und funktioniert nicht mit variablen Argumenten.

(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
quelle