online - clojure vs scala
Clojure: reducir vs. aplicar (9)
Entiendo la diferencia conceptual entre reduce
y 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 el clojure más idiomático? ¿Hace mucha diferencia de una manera u otra? A partir de mis pruebas de rendimiento (limitadas), parece que reduce
es un poco más rápido.
Cuando se usa una función simple como +, realmente no importa cuál usar.
En general, la idea es que reducir es una operación de acumulación. Usted presenta el valor de acumulación actual y un nuevo valor para su función de acumulación, con el resultado del valor acumulado para la siguiente 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 esperando una cantidad de argumentos escalares, pero actualmente están en una colección y deben retirarse. Entonces, en lugar de decir:
vals = [ val1 val2 val3 ]
(some-fn (vals 0) (vals 1) (vals2))
podemos decir:
(apply some-fn vals)
y se convierte para ser equivalente a:
(some-fn val1 val2 val3)
Entonces, usar "aplicar" es como "eliminar los paréntesis" alrededor de la secuencia.
En este caso específico, prefiero reduce
porque es más legible : cuando leo
(reduce + some-numbers)
Sé de inmediato que estás convirtiendo una secuencia en un valor.
Con apply
tengo que considerar qué función se está aplicando: "ah, es la función +
, así que obtengo ... un solo número". Un poco menos directo.
La belleza de aplicar función dada (+ en este caso) se puede aplicar a la lista de argumentos formada por argumentos intermedios previos con una colección final. Reducir es una abstracción para procesar los elementos de la colección aplicando la función para cada uno y no funciona con el caso variable args.
(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)
Las opiniones varían: en el mundo de Lisp mayor, reduce
definitivamente se considera más idiomático. Primero, están los problemas variados ya discutidos. Además, algunos compiladores de Common Lisp fallarán cuando apply
se aplique a listas muy largas debido a la forma en que manejan las listas de argumentos.
Entre los Clojuristas en mi círculo, sin embargo, el uso de apply
en este caso parece más común. Me resulta más fácil grok y también lo prefiero.
No importa en este caso, porque + es un caso especial que puede aplicarse a cualquier cantidad de argumentos. Reducir es una forma de aplicar una función que espera una cantidad fija de argumentos (2) a una lista arbitrariamente larga de argumentos.
Normalmente me encuentro prefiriendo reducir cuando actúo en cualquier tipo de colección: funciona bien y es una función bastante útil en general.
La razón principal por la que utilizaría apply 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)
Para los novatos que miran esta respuesta,
ten 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}
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 pacheo 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
Al observar el código fuente de clojure reducir su recursión bastante limpia con internal-reduce, no se encontró nada en la implementación de apply. La implementación de Clojure de + para aplicar internamente invoca reduce, que está almacenada en caché por repl, que parece explicar la cuarta llamada. ¿Alguien puede aclarar qué está pasando realmente aquí?
reduce
y apply
son, por supuesto, solo equivalentes (en términos del resultado final devuelto) para las funciones asociativas que necesitan ver todos sus argumentos en el caso de ariadia variable. Cuando son equivalentes a los resultados, yo diría que apply
es siempre perfectamente idiomático, mientras que reduce
es equivalente, y podría reducirse a una fracción de un abrir y cerrar de ojos, en muchos casos comunes. Lo que sigue es mi razonamiento para creer esto.
+
se implementa en sí mismo en términos de reduce
para el caso de aria variable (más de 2 argumentos). De hecho, esto parece una forma "predeterminada" muy sensible para cualquier función asociativa de aria variable: reduce
tiene el potencial de realizar algunas optimizaciones para acelerar las cosas, quizás a través de algo como internal-reduce
, una novedad 1.2 deshabilitada recientemente en el maestro, pero ojalá sea reintroducido en el futuro, lo cual sería una tontería repetir en cada función que podría beneficiarse de ellos en el caso vararg. En casos comunes, apply
solo agregará un poco de sobrecarga. (Tenga en cuenta que no hay nada de qué preocuparse realmente).
Por otro lado, una función compleja puede aprovechar algunas oportunidades de optimización que no son lo suficientemente generales como para incorporarlas a la reduce
; luego apply
te permitiría aprovechar esos, mientras que reduce
podría en realidad hacerte más lento. Un buen ejemplo de este último escenario que ocurre en la práctica lo proporciona str
: utiliza un StringBuilder
internamente y se beneficiará significativamente del uso de apply
lugar de reduce
.
Por lo tanto, yo diría que use apply
cuando tenga dudas; y si usted sabe que no le está comprando nada en lugar de reduce
(y que es poco probable que cambie muy pronto), puede usar reduce
para reduce
esos gastos indirectos innecesarios si así lo desea.