web-dev-qa-db-fra.com

Clojure: réduire vs appliquer

Je comprends la différence conceptuelle entre reduce et 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)

Cependant, lequel est le clojure le plus idiomatique? Cela fait-il une grande différence dans un sens ou dans l'autre? D'après mes tests de performances (limités), il semble que reduce soit un peu plus rapide.

122
dbyrne

reduce et apply ne sont bien sûr équivalents (en termes de résultat final retourné) que pour les fonctions associatives qui ont besoin de voir tous leurs arguments dans le cas de l'arité variable. Quand ils sont équivalents en termes de résultats, je dirais que apply est toujours parfaitement idiomatique, tandis que reduce est équivalent - et pourrait raser une fraction de clin d'œil - dans beaucoup de cas courants. Ce qui suit est ma justification pour croire cela.

+ est lui-même implémenté en termes de reduce pour le cas d'arité variable (plus de 2 arguments). En effet, cela semble être un moyen "par défaut" extrêmement sensé pour toute fonction associative à arité variable: reduce a le potentiel d'effectuer certaines optimisations pour accélérer les choses - peut-être à travers quelque chose comme internal-reduce, une nouveauté 1.2 récemment désactivée dans master, mais qui, espérons-le, sera réintroduite à l'avenir - qu'il serait idiot de reproduire dans toutes les fonctions qui pourraient en bénéficier dans le cas vararg. Dans de tels cas courants, apply ajoutera juste un peu de surcharge. (Notez qu'il n'y a rien de vraiment inquiétant.)

D'un autre côté, une fonction complexe pourrait profiter de certaines opportunités d'optimisation qui ne sont pas assez générales pour être intégrées dans reduce; alors apply vous permettrait d'en profiter tandis que reduce pourrait en fait vous ralentir. Un bon exemple de ce dernier scénario se produisant dans la pratique est fourni par str: il utilise un StringBuilder en interne et bénéficiera considérablement de l'utilisation de apply plutôt que reduce.

Donc, je dirais utiliser apply en cas de doute; et s'il vous arrive de savoir qu'il ne vous achète rien de plus que reduce (et qu'il est peu probable que cela change très bientôt), n'hésitez pas à utiliser reduce pour raser cette diminution des frais généraux inutiles si vous envie.

118
Michał Marczyk

Pour les débutants qui regardent cette réponse,
attention, ce ne sont pas les mêmes:

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

Les opinions varient - Dans le plus grand monde LISP, reduce est définitivement considéré comme plus idiomatique. Premièrement, il y a les questions variadiques déjà discutées. En outre, certains compilateurs LISP courants échouent réellement lorsque apply est appliqué à des listes très longues en raison de la façon dont ils gèrent les listes d'arguments.

Parmi les clojuristes de mon entourage, l'utilisation de apply dans ce cas semble plus courante. Je trouve plus facile de grogner et je le préfère aussi.

20
drcode

Cela ne fait aucune différence dans ce cas, car + est un cas spécial qui peut s'appliquer à n'importe quel nombre d'arguments. Réduire est un moyen d'appliquer une fonction qui attend un nombre fixe d'arguments (2) à une liste d'arguments arbitrairement longue.

19
G__

Normalement, je préfère réduire lorsque j'agis sur n'importe quel type de collection - il fonctionne bien et est une fonction assez utile en général.

La principale raison pour laquelle j'appliquerais est si les paramètres signifient différentes choses dans différentes positions, ou si vous avez quelques paramètres initiaux mais que vous souhaitez obtenir le reste d'une collection, par ex.

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

Dans ce cas précis, je préfère reduce parce que c'est plus lisible: quand je lis

(reduce + some-numbers)

Je sais immédiatement que vous transformez une séquence en valeur.

Avec apply je dois considérer quelle fonction est appliquée: "ah, c'est le + fonction, donc je reçois ... un seul numéro ". Un peu moins simple.

8
mascip

Lorsque vous utilisez une fonction simple comme +, peu importe celle que vous utilisez.

En général, l'idée est que reduce est une opération d'accumulation. Vous présentez la valeur d'accumulation actuelle et une nouvelle valeur à votre fonction d'accumulation. Le résultat de la fonction est la valeur cumulée pour la prochaine itération. Ainsi, vos itérations ressemblent à:

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

Pour appliquer, l'idée est que vous essayez d'appeler une fonction en attendant un certain nombre d'arguments scalaires, mais ils sont actuellement dans une collection et doivent être extraits. Donc, au lieu de dire:

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

on peut dire:

(apply some-fn vals)

et il est converti pour être équivalent à:

(some-fn val1 val2 val3)

Donc, utiliser "appliquer" revient à "supprimer les parenthèses" autour de la séquence.

6
Alan Thompson

Un peu en retard sur le sujet mais j'ai fait une expérience simple après avoir lu cet exemple. Voici le résultat de ma réponse, je ne peux rien déduire de la réponse, mais il semble qu'il y ait une sorte de coup de cache entre réduire et appliquer.

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

En regardant le code source de clojure réduire sa récursion assez propre avec internal-Reduce, je n'ai rien trouvé sur l'implémentation de apply. L'implémentation de Clojure de + pour appliquer en interne appelle réduire, qui est mis en cache par repl, ce qui semble expliquer le 4ème appel. Quelqu'un peut-il clarifier ce qui se passe vraiment ici?

4
rohit

La beauté de apply est donnée une fonction (+ dans ce cas) peut être appliquée à une liste d'arguments formée par des arguments intermédiaires pré-en attente avec une collection de fin. Reduce est une abstraction pour traiter les éléments de collection en appliquant la fonction pour chacun et ne fonctionne pas avec le cas des arguments variables.

(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)
3
Ira