2010-12-27 17 views
11

Con frecuencia me resulta deseable ejecutar de manera eficiente una función Clojure varias veces con un índice entero (como "dotimes") pero también obtengo los resultados como una secuencia/lista lista (como "para").¿Se cruzan entre las funciones "dotimes" y "for"?

es decir, me gustaría hacer algo como esto:

(fortimes [i 10] (* i i)) 

=> (0 1 4 9 16 25 36 49 64 81) 

claro que sería posible hacerlo:

(for [i (range 10)] (* i i)) 

Pero me gustaría evitar la creación y tirar el temporal lista de rango si es posible.

¿Cuál es la mejor manera de lograr esto en Clojure?

+0

¿Cuál es la última pregunta sobre esta pregunta? ¿Es clj-iterate la mejor solución o hay mejores alternativas? – jcheat

Respuesta

5

he escrito una macro iteración que puede hacer esto y otros tipos de iteración de manera muy eficiente. El paquete se llama clj-iterate, tanto en github como en clojars. Por ejemplo:

user> (iter {for i from 0 to 10} {collect (* i i)}) 
(0 1 4 9 16 25 36 49 64 81 100) 

Esto no creará una lista temporal.

+0

+1 por una pequeña herramienta. como una cuestión de interés, ¿cómo se las arregla para evitar la lista temporal? – mikera

6

Generar un rango en un bucle for, como se muestra en el segundo ejemplo, es la solución idiomática para resolver este problema en Clojure.

Dado que Clojure se basa en el paradigma funcional, la programación en Clojure, de forma predeterminada, generará estructuras temporales de datos como esta. Sin embargo, dado que tanto el comando "rango" como el comando "para" operan con secuencias perezosas, escribir este código no obliga a que toda la estructura de datos de rango temporal exista en la memoria a la vez. Si se usa correctamente, existe una sobrecarga de memoria muy baja para los seqs perezosos como se usa en este ejemplo. Además, la sobrecarga de cómputo para su ejemplo es modesta y solo debería crecer linealmente con el tamaño del rango. Esto se considera una sobrecarga aceptable para el código típico de Clojure.

La forma adecuada de evitar por completo esta sobrecarga, si la lista de rango temporal es absolutamente, positivamente inaceptable para su situación, es escribir su código utilizando átomos o transitorios: http://clojure.org/transients. Sin embargo, si hace esto, renunciará a muchas de las ventajas del modelo de programación Clojure a cambio de un rendimiento ligeramente mejor.

3

No estoy seguro de por qué le preocupa "crear y tirar" la secuencia diferida creada por la función range. La iteración acotada realizada por dotimes es probablemente más eficiente, ya que se trata de un incremento en línea y se compara con cada paso, pero puede pagar un costo adicional para expresar allí su propia concatenación de listas.

La solución típica de Lisp es anteponer nuevos elementos a una lista que construye sobre la marcha, luego invierta esa lista acumulada de forma destructiva para obtener el valor de retorno. Son bien conocidas otras técnicas para permitir agregar a una lista en tiempo constante, pero no siempre resultan ser más eficientes que el enfoque de antes de invertir.

En Clojure, puede utilizar transitorios para llegar allí, confiando en el comportamiento destructivo de la función conj!:

(let [r (transient [])] 
    (dotimes [i 10] 
    (conj! r (* i i))) ;; destructive 
    (persistent! r)) 

que parece funcionar, pero the documentation on transients advierte que no se debe utilizar conj! a " bash values ​​in place "— es decir, contar con un comportamiento destructivo en lugar de capturar el valor de retorno. Por lo tanto, esa forma debe ser reescrita.

Con el fin de volver a enlazar r anterior al nuevo valor producido por cada llamada a conj!, tendríamos que utilizar un atom para introducir un nivel de indirección más.En ese punto, sin embargo, solo estamos luchando contra dotimes, y sería mejor escribir su propio formulario usando loop y recur.

Sería bueno poder preasignar el vector para que sea del mismo tamaño que el límite de la iteración. No veo una manera de hacerlo.

+0

la razón principal para evitar crear y tirar secuencias adicionales es minimizar la basura. sí, sé que la recolección de basura es realmente muy barata hoy en día, pero causa problemas de latencia/pausa de GC que son un problema real en algunas aplicaciones – mikera

3
(defmacro fortimes [[i end] & code] 
    `(let [finish# ~end] 
    (loop [~i 0 results# '()] 
     (if (< ~i finish#) 
     (recur (inc ~i) (cons [email protected] results#)) 
     (reverse results#))))) 

ejemplo:

(fortimes [x 10] (* x x)) 

da:

(0 1 4 9 16 25 36 49 64 81) 
+0

Gracias John :-)! Esa es una pequeña solución muy elegante. Aunque todavía crea una lista temporal innecesaria antes de hacer lo inverso, ¿verdad? ¿Alguna forma de evitar eso? Estoy pensando que podría tener sentido ejecutar el código en orden inverso, aunque eso podría confundir con los efectos secundarios ... – mikera

1

Hmm, parece que no puede responder a su comentario porque no estaba registrado. Sin embargo, clj-iterate usa un PersistentQueue, que es parte de la biblioteca de tiempo de ejecución, pero no está expuesto a través del lector.

Básicamente es una lista en la que puedes unir hasta el final.

Cuestiones relacionadas