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.
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.
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}
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.
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.
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)
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.
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.
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?
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)