Clojure: reducir vs aplicar

126

Entiendo la diferencia conceptual entre reducey 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)

Sin embargo, ¿cuál es la forma más idiomática? ¿Hace mucha diferencia de una forma u otra? Según mis pruebas de rendimiento (limitadas), parece que reducees un poco más rápido.

dbyrne
fuente

Respuestas:

125

reducey apply, por supuesto, solo son equivalentes (en términos del resultado final devuelto) para funciones asociativas que necesitan ver todos sus argumentos en el caso de aridad variable. Cuando son equivalentes en cuanto a resultados, diría que applysiempre es perfectamente idiomático, mientras que reducees equivalente, y podría reducir una fracción de un abrir y cerrar de ojos, en muchos de los casos comunes. Lo que sigue es mi justificación para creer esto.

+se implementa en términos de reducepara el caso de aridad variable (más de 2 argumentos). De hecho, esto parece una forma "predeterminada" inmensamente sensata para cualquier función asociativa de aridad variable: reducetiene el potencial de realizar algunas optimizaciones para acelerar las cosas, tal vez a través de algo como internal-reduce, una novedad 1.2 recientemente deshabilitada en master, pero es de esperar que se reintroduzca en el futuro, lo cual sería una tontería replicar en todas las funciones que podrían beneficiarse de ellas en el caso de vararg. En tales casos comunes, applysolo agregará un poco de sobrecarga. (Tenga en cuenta que no hay nada de qué preocuparse realmente).

Por otro lado, una función compleja podría aprovechar algunas oportunidades de optimización que no son lo suficientemente generales como para ser incorporadas reduce; entonces applyte permitiría aprovecharlos, mientras reduceque en realidad podría ralentizarte. Un buen ejemplo de este último escenario que ocurre en la práctica lo proporciona str: utiliza StringBuilderinternamente y se beneficiará significativamente del uso de en applylugar de reduce.

Entonces, yo diría usar applycuando tengas dudas; y si sabe que no le está comprando nada reduce(y que es poco probable que esto cambie muy pronto), siéntase libre de usar reducepara reducir esa diminuta sobrecarga innecesaria si lo desea.

Michał Marczyk
fuente
Gran respuesta. En una nota al margen, ¿por qué no incluir una sumfunción incorporada como en Haskell? Parece una operación bastante común.
dbyrne 01 de
17
Gracias, feliz de escuchar eso! Re:, sumyo diría que Clojure tiene esta función, se llama +y puedes usarla apply. :-) Hablando en serio, creo que en Lisp, en general, si se proporciona una función variada, no suele ir acompañada de un contenedor que opere en colecciones, para eso es lo que usa apply(o reduce, si sabe que tiene más sentido).
Michał Marczyk 01 de
66
Curioso, mi consejo es todo lo contrario: cuando tenga reducedudas, applycuando sepa con certeza que hay una optimización. reduceEl contrato es más preciso y, por lo tanto, más propenso a la optimización general. applyes más vago y, por lo tanto, solo puede optimizarse caso por caso. stry concatson las dos excepciones prevalentes.
cgrand
1
@cgrand Una reformulación de mi justificación podría ser más o menos que para las funciones donde reducey applyson equivalentes en términos de resultados, esperaría que el autor de la función en cuestión sepa la mejor manera de optimizar su sobrecarga variable y simplemente implementarla en términos de reducesi de hecho, eso es lo que tiene más sentido (la opción para hacerlo está siempre disponible y es un valor predeterminado muy sensato). Sin embargo, veo de dónde vienes, reducees definitivamente central en la historia de rendimiento de Clojure (y cada vez más), muy altamente optimizado y muy claramente especificado.
Michał Marczyk
51

Para los novatos que miran esta respuesta,
tengan cuidado, no son lo mismo:

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

Las opiniones varían: en el gran mundo de Lisp, reducedefinitivamente se considera más idiomático. Primero, están los problemas variados ya discutidos. Además, algunos compiladores de Common Lisp realmente fallarán cuando applyse apliquen a listas muy largas debido a cómo manejan las listas de argumentos.

Sin embargo, entre los Clojurists en mi círculo, usar applyen este caso parece más común. Me resulta más fácil asimilar y también lo prefiero.

drcode
fuente
19

No hace la diferencia en este caso, porque + es un caso especial que puede aplicarse a cualquier número de argumentos. Reducir es una forma de aplicar una función que espera un número fijo de argumentos (2) a una lista arbitrariamente larga de argumentos.

GRAMO__
fuente
9

Normalmente me encuentro prefiriendo reducir cuando actúo sobre cualquier tipo de colección: funciona bien y es una función bastante útil en general.

La razón principal por la que usaría aplicar es si los parámetros significan cosas diferentes en diferentes posiciones, o si tiene un par de parámetros iniciales pero desea obtener el resto de una colección, por ejemplo

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

En este caso específico prefiero reduceporque es más legible : cuando leo

(reduce + some-numbers)

Sé de inmediato que estás convirtiendo una secuencia en un valor.

Con applytengo que considerar qué función se está aplicando: "ah, es la +función, así que obtengo ... un solo número". Ligeramente menos directo.

mascota
fuente
7

Cuando se usa una función simple como +, realmente no importa cuál uses.

En general, la idea es que reducees una operación acumulativa. Presente el valor de acumulación actual y un nuevo valor para su función de acumulación. El resultado de la función es el valor acumulativo para la próxima iteración. Entonces, sus iteraciones se ven así:

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

Para aplicar, la idea es que está intentando llamar a una función que espera una serie de argumentos escalares, pero actualmente se encuentran en una colección y deben retirarse. Entonces, en lugar de decir:

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

podemos decir:

(apply some-fn vals)

y se convierte para ser equivalente a:

(some-fn val1 val2 val3)

Entonces, usar "aplicar" es como "quitar los paréntesis" alrededor de la secuencia.

Alan Thompson
fuente
4

Un poco tarde en el tema, pero hice un experimento simple después de leer este ejemplo. Aquí está el resultado de mi respuesta, simplemente no puedo deducir nada de la respuesta, pero parece que hay algún tipo de patada de almacenamiento en caché entre reducir y aplicar.

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

Mirando el código fuente de clojure reduce su recursión bastante limpia con internal-reduce, sin embargo, no encontré nada sobre la implementación de apply. La implementación de Clojure de + para aplicar internamente invocar reduce, que se almacena en caché por repl, que parecen explicar la cuarta llamada. ¿Alguien puede aclarar lo que realmente está sucediendo aquí?

rohit
fuente
Sé que preferiría reducir cada vez que pueda :)
rohit
2
No debe poner la rangellamada dentro del timeformulario. Colóquelo afuera para eliminar la interferencia de la construcción de la secuencia. En mi caso, reduceconsistentemente supera apply.
Davyzhu el
3

La belleza de la aplicación se le da función (+ en este caso) se puede aplicar a la lista de argumentos formada por argumentos intermedios pre-pendientes con una colección final. Reducir es una abstracción para procesar elementos de colección que aplican la función para cada uno y no funciona con argumentos 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)
Ira
fuente