Clojure: réduire vs appliquer


126

Je comprends la différence conceptuelle entre reduceet 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 plus idiomatique clojure? 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 reducec'est un peu plus rapide.

Réponses:


125

reduceet ne applysont bien sûr équivalentes (en termes de résultat final renvoyé) 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 applyc'est toujours parfaitement idiomatique, alors que cela reduceest équivalent - et pourrait raser une fraction d'un clin d'œil - dans de nombreux cas courants. Ce qui suit est ma raison de croire cela.

+est lui-même implémenté en termes de reducepour le cas d'arité variable (plus de 2 arguments). En effet, cela semble être une manière "par défaut" extrêmement sensible d'aller pour toute fonction associative à arité variable: reducea le potentiel d'effectuer des optimisations pour accélérer les choses - peut-être par quelque chose comme internal-reduce, une nouveauté 1.2 récemment désactivée dans master, mais j'espère être réintroduit dans le futur - qu'il serait ridicule de reproduire dans toutes les fonctions qui pourraient en bénéficier dans le cas vararg. Dans ces cas courants, applyajoutera simplement un peu de frais généraux. (Notez qu'il n'y a pas de quoi s'inquiéter vraiment.)

D'un autre côté, une fonction complexe peut tirer parti de certaines opportunités d'optimisation qui ne sont pas assez générales pour être intégrées reduce; alors applyvous permettrait de profiter de ceux-ci tout reduceen vous ralentissant. Un bon exemple de ce dernier scénario se produisant dans la pratique est fourni par str: il utilise un en StringBuilderinterne et bénéficiera considérablement de l'utilisation de applyplutôt que reduce.

Donc, je dirais utiliser en applycas de doute; et si vous savez que cela ne vous achète rien reduce(et que cela ne changera probablement pas très bientôt), n'hésitez pas à utiliser reducepour raser cette minuscule surcharge inutile si vous en avez envie.


Très bonne réponse. Sur une note latérale, pourquoi ne pas inclure une sumfonction intégrée comme dans haskell? Cela semble être une opération assez courante.
dbyrne

17
Merci, heureux de l'entendre! Re sum:, je dirais que Clojure a cette fonction, elle est appelée +et vous pouvez l'utiliser avec apply. :-) Sérieusement, je pense qu'en Lisp, en général, si une fonction variadique est fournie, elle n'est généralement pas accompagnée d'un wrapper fonctionnant sur des collections - c'est ce que vous utilisez applypour (ou reduce, si vous savez que cela a plus de sens).
Michał Marczyk

6
C'est drôle, mon conseil est le contraire: en reducecas de doute, applyquand on est sûr qu'il y a une optimisation. reduceLe contrat est plus précis et donc plus sujet à une optimisation générale. applyest plus vague et ne peut donc être optimisée qu'au cas par cas. stret concatsont les deux exceptions courantes.
cgrand

1
@cgrand Une reformulation de ma justification pourrait être à peu près que pour les fonctions où reduceet applysont équivalentes en termes de résultats, je m'attendrais à ce que l'auteur de la fonction en question sache comment optimiser au mieux leur surcharge variadique et l'implémenter simplement en termes de reducesi c'est en effet ce qui a le plus de sens (l'option pour le faire est certainement toujours disponible et constitue un défaut éminemment raisonnable). Je vois cependant d'où vous venez, reducec'est définitivement au cœur de l'histoire de la performance de Clojure (et de plus en plus), très hautement optimisée et très clairement spécifiée.
Michał Marczyk

51

Pour les débutants qui regardent cette réponse,
faites 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}

21

Les opinions varient - Dans le plus grand monde Lisp, reduceest définitivement considéré comme plus idiomatique. Premièrement, il y a les problèmes variadiques déjà discutés. De plus, certains compilateurs Common Lisp échoueront en fait lorsqu'ils applysont appliqués à de très longues listes en raison de la façon dont ils gèrent les listes d'arguments.

Parmi les Clojurists de mon entourage, cependant, l'utilisation applydans ce cas semble plus courante. Je trouve plus facile de grok et je préfère ça aussi.


19

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.


9

Je préfère généralement réduire lorsque j'agis sur n'importe quel type de collection - cela fonctionne bien et c'est une fonction assez utile en général.

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

(apply + 1 2 other-number-list)

9

Dans ce cas précis je préfère reducecar c'est plus lisible : quand je lis

(reduce + some-numbers)

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

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


7

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

En général, l'idée est qu'il reduces'agit d'une opération cumulative. 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 l'itération suivante. Ainsi, vos itérations ressemblent à:

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

Pour apply, 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 retirés. 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.


4

Un peu tard sur le sujet mais j'ai fait une expérience simple après avoir lu cet exemple. Voici le résultat de ma réplique, je ne peux tout simplement rien déduire de la réponse, mais il semble qu'il y ait une sorte de coup de pied de mise en 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écursivité assez propre avec internal-Reduce, rien n'a été trouvé sur l'implémentation de apply. L'implémentation de Clojure de + pour apply en interne appelle la réduction, qui est mise en cache par repl, ce qui semble expliquer le 4ème appel. Quelqu'un peut-il clarifier ce qui se passe réellement ici?


Je sais que je préférerai réduire quand je peux :)
rohit

2
Vous ne devriez pas mettre l' rangeappel dans le timeformulaire. Mettez-le à l'extérieur pour supprimer l'interférence de la construction de la séquence. Dans mon cas, reducesurpasse constamment apply.
Davyzhu

3

La beauté de la fonction apply est donnée (+ 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. Réduire est une abstraction pour traiter les éléments de collection en appliquant la fonction pour chacun et ne fonctionne pas avec 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)
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.