online - clojure vs scala
Errores de programaciĆ³n comunes para los desarrolladores de Clojure para evitar (8)
Olvidando forzar la evaluación de los seces perezosos
Lazy seqs no se evalúa a menos que solicite su evaluación. Puede esperar que esto imprima algo, pero no es así.
user=> (defn foo [] (map println [:foo :bar]) nil)
#''user/foo
user=> (foo)
nil
El map
nunca se evalúa, se descarta silenciosamente porque es flojo. Tienes que usar uno de doseq
, dorun
, doall
etc. para forzar la evaluación de las secuencias perezosas de los efectos colaterales.
user=> (defn foo [] (doseq [x [:foo :bar]] (println x)) nil)
#''user/foo
user=> (foo)
:foo
:bar
nil
user=> (defn foo [] (dorun (map println [:foo :bar])) nil)
#''user/foo
user=> (foo)
:foo
:bar
nil
Usando un map
desnudo en el tipo de REPL parece que funciona, pero solo funciona porque el REPL obliga a la evaluación de los seqs perezosos. Esto puede hacer que el error sea aún más difícil de detectar, porque su código funciona en el REPL y no funciona desde un archivo fuente o dentro de una función.
user=> (map println [:foo :bar])
(:foo
:bar
nil nil)
¿Cuáles son algunos de los errores más comunes cometidos por los desarrolladores de Clojure y cómo podemos evitarlos?
Por ejemplo; los recién llegados a Clojure piensan que el contains?
la función funciona igual que java.util.Collection#contains
. Sin embargo, contains?
solo funcionará de manera similar cuando se utiliza con colecciones indexadas como mapas y conjuntos y está buscando una clave determinada:
(contains? {:a 1 :b 2} :b)
;=> true
(contains? {:a 1 :b 2} 2)
;=> false
(contains? #{:a 1 :b 2} :b)
;=> true
Cuando se utiliza con colecciones indexadas numéricamente (vectores, matrices) contains?
solo comprueba que el elemento dado se encuentre dentro del rango válido de índices (basado en cero):
(contains? [1 2 3 4] 4)
;=> false
(contains? [1 2 3 4] 0)
;=> true
Si se le da una lista, contains?
nunca volverá verdadero.
Muchas cosas ya mencionadas. Añadiré uno más.
Clojure si trata los objetos Booleanos Java siempre como verdaderos, incluso si su valor es falso. Así que si tienes una función de Java que devuelve un valor booleano java, asegúrate de no verificarlo directamente (if java-bool "Yes" "No")
sino (if (boolean java-bool) "Yes" "No")
.
Me quemé con esto con la biblioteca clojure.contrib.sql que devuelve los campos booleanos de la base de datos como objetos booleanos java.
Soy un novato de Clojure. Los usuarios más avanzados pueden tener problemas más interesantes.
tratando de imprimir infinitas secuencias perezosas.
Sabía lo que estaba haciendo con mis secuencias perezosa, pero con fines de depuración inserté algunas llamadas de impresión / prn / pr, olvidándome temporalmente de lo que estaba imprimiendo. Gracioso, ¿por qué mi PC colgó todo?
tratando de programar Clojure imperativamente.
Existe la tentación de crear una gran cantidad de ref
o atom
sy escribir código que constantemente se ensucia con su estado. Esto se puede hacer, pero no es una buena opción. También puede tener un rendimiento deficiente y rara vez se beneficia de múltiples núcleos.
tratando de programar Clojure al 100% funcionalmente.
Un aspecto opuesto a esto: algunos algoritmos realmente quieren un poco de estado mutable. Evitar religiosamente el estado mutable a toda costa puede dar como resultado algoritmos lentos o torpes. Se necesita juicio y un poco de experiencia para tomar la decisión.
tratando de hacer demasiado en Java.
Debido a que es tan fácil llegar a Java, a veces es tentador usar Clojure como un envoltorio de lenguaje de scripts alrededor de Java. Ciertamente, necesitará hacer exactamente esto cuando use la funcionalidad de la biblioteca Java, pero tiene poco sentido (por ejemplo) mantener las estructuras de datos en Java o usar tipos de datos Java, como colecciones para las cuales hay buenos equivalentes en Clojure.
demasiadas parantheses, especialmente con la llamada al método void java dentro que da como resultado NPE:
public void foo() {}
((.foo))
da como resultado NPE de parantheses exteriores porque las parantheses internas se evalúan a nil.
public int bar() { return 5; }
((.bar))
resultados en la más fácil de depurar:
java.lang.Integer cannot be cast to clojure.lang.IFn
[Thrown class java.lang.ClassCastException]
usando loop ... recur
a las secuencias de proceso cuando el mapa funciona.
(defn work [data]
(do-stuff (first data))
(recur (rest data)))
vs.
(map do-stuff data)
La función de mapa (en la última rama) usa secuencias fragmentadas y muchas otras optimizaciones. Además, debido a que esta función se ejecuta con frecuencia, el JIT de Hotspot generalmente lo tiene optimizado y listo para funcionar sin ningún "tiempo de calentamiento".
Los tipos de colección tienen comportamientos diferentes para algunas operaciones:
user=> (conj ''(1 2 3) 4)
(4 1 2 3) ;; new element at the front
user=> (conj [1 2 3] 4)
[1 2 3 4] ;; new element at the back
user=> (into ''(3 4) (list 5 6 7))
(7 6 5 3 4)
user=> (into [3 4] (list 5 6 7))
[3 4 5 6 7]
Trabajar con cadenas puede ser confuso (todavía no las obtengo del todo). Específicamente, las cadenas no son lo mismo que las secuencias de caracteres, aunque las funciones de secuencia funcionen en ellas:
user=> (filter #(> (int %) 96) "abcdABCDefghEFGH")
(/a /b /c /d /e /f /g /h)
Para volver a sacar una cadena, deberías hacer:
user=> (apply str (filter #(> (int %) 96) "abcdABCDefghEFGH"))
"abcdefgh"
Manteniendo la cabeza en bucles.
Se corre el riesgo de quedarse sin memoria si recorre los elementos de una secuencia vaga potencialmente muy grande o infinita mientras mantiene una referencia al primer elemento.
Olvidando que no hay TCO.
Las llamadas de cola regulares consumen espacio de pila, y se desbordarán si no tiene cuidado. Clojure tiene ''recur
y ''trampoline
para manejar muchos de los casos en los que se utilizarían llamadas de cola optimizadas en otros idiomas, pero estas técnicas deben aplicarse intencionalmente.
Secuencias no muy perezosas.
Puedes construir una secuencia perezosa con ''lazy-seq
o ''lazy-cons
(o construyendo sobre API de nivel más alto), pero si lo envuelves en ''vec
o lo pasas a través de alguna otra función que realice la secuencia, entonces lo hará ya no ser perezoso Tanto la pila como el montón pueden ser desbordados por esto.
Poniendo cosas mutables en refs.
Puedes hacerlo técnicamente, pero solo la referencia del objeto en la ref sí está gobernada por el STM, no el objeto referido y sus campos (a menos que sean inmutables y apunten a otros refs). Por lo tanto, siempre que sea posible, prefiera solo objetos inmutables en refs. Lo mismo vale para los átomos.
Octals literales
En un momento dado estaba leyendo en una matriz que utilizaba ceros iniciales para mantener filas y columnas adecuadas. Matemáticamente, esto es correcto, ya que el cero inicial obviamente no altera el valor subyacente. Los intentos de definir una var con esta matriz, sin embargo, fallarían misteriosamente con:
java.lang.NumberFormatException: Invalid number: 08
lo cual me desconcertó por completo La razón es que Clojure trata los valores enteros literales con ceros a la izquierda como octales, y no hay número 08 en octal.
También debo mencionar que Clojure admite valores hexadecimales Java tradicionales a través del prefijo 0x . También puede usar cualquier base entre 2 y 36 usando la notación "base + r +", como 2r101010 o 36r16 que son 42 base diez.
Intentando devolver literales en una función anónima literal
Esto funciona:
user> (defn foo [key val]
{key val})
#''user/foo
user> (foo :a 1)
{:a 1}
así que creí que esto también funcionaría:
(#({%1 %2}) :a 1)
pero falla con:
java.lang.IllegalArgumentException: Wrong number of args passed to: PersistentArrayMap
porque la macro del lector # () se expande a
(fn [%1 %2] ({%1 %2}))
con el literal del mapa envuelto entre paréntesis. Como es el primer elemento, se trata como una función (que en realidad es un mapa literal), pero no se proporcionan argumentos necesarios (como una clave). En resumen, la función anónima literal no se expande a
(fn [%1 %2] {%1 %2}) ; notice the lack of parenthesis
y entonces no puede tener ningún valor literal ([],: a, 4,%) como el cuerpo de la función anónima.
Se han dado dos soluciones en los comentarios. Brian Carper sugiere usar constructores de implementación de secuencias (matriz-mapa, conjunto-hash, vector) así:
(#(array-map %1 %2) :a 1)
mientras que Dan muestra que puede usar la función de identity para desenvolver el paréntesis externo:
(#(identity {%1 %2}) :a 1)
La sugerencia de Brian en realidad me lleva a mi siguiente error ...
Pensando que hash-map o array-map determinan la implementación del mapa concreto e inmutable
Considera lo siguiente:
user> (class (hash-map))
clojure.lang.PersistentArrayMap
user> (class (hash-map :a 1))
clojure.lang.PersistentHashMap
user> (class (assoc (apply array-map (range 2000)) :a :1))
clojure.lang.PersistentHashMap
Aunque generalmente no tendrá que preocuparse por la implementación concreta de un mapa Clojure, debe saber que las funciones que hacen crecer un mapa, como assoc o conj , pueden tomar un PersistentArrayMap y devolver un PersistentHashMap , que funciona más rápido para mapas más grandes.
Usar una función como punto de recursión en lugar de un loop para proporcionar enlaces iniciales
Cuando comencé, escribí muchas funciones como esta:
; Project Euler #3
(defn p3
([] (p3 775147 600851475143 3))
([i n times]
(if (and (divides? i n) (fast-prime? i times)) i
(recur (dec i) n times))))
Cuando en realidad el loop hubiera sido más conciso e idiomático para esta función en particular:
; Elapsed time: 387 msecs
(defn p3 [] {:post [(= % 6857)]}
(loop [i 775147 n 600851475143 times 3]
(if (and (divides? i n) (fast-prime? i times)) i
(recur (dec i) n times))))
Observe que reemplacé el argumento vacío, el cuerpo de la función "constructor por defecto" (p3 775147 600851475143 3) con un bucle + enlace inicial. El recurrir ahora vuelve a enlazar los enlaces de bucle (en lugar de los parámetros fn) y salta de nuevo al punto de recursión (bucle, en lugar de fn).
Hace referencia a vars "fantasmas"
Estoy hablando sobre el tipo de var que puede definir usando REPL, durante su programación exploratoria, y luego, sin saberlo, la referencia en su fuente. Todo funciona bien hasta que vuelva a cargar el espacio de nombres (tal vez cerrando su editor) y luego descubra un montón de símbolos independientes a los que se hace referencia a lo largo de su código. Esto también ocurre con frecuencia cuando estás refactorizando, moviendo una var de un espacio de nombres a otro.
Tratar la lista de comprensión como un imperativo para el ciclo
Básicamente, estás creando una lista diferida basada en listas existentes en lugar de simplemente realizar un ciclo controlado. La doseq de Clojure es en realidad más análoga a las construcciones imperativas de bucle foreach.
Un ejemplo de cómo son diferentes es la capacidad de filtrar los elementos sobre los que iteran utilizando predicados arbitrarios:
user> (for [n ''(1 2 3 4) :when (even? n)] n)
(2 4)
user> (for [n ''(4 3 2 1) :while (even? n)] n)
(4)
Otra forma en que son diferentes es que pueden operar en infinitas secuencias perezosas:
user> (take 5 (for [x (iterate inc 0) :when (> (* x x) 3)] (* 2 x)))
(4 6 8 10 12)
También pueden manejar más de una expresión vinculante, iterando sobre la expresión más a la derecha primero y trabajando a la izquierda:
user> (for [x ''(1 2 3) y ''(/a /b /c)] (str x y))
("1a" "1b" "1c" "2a" "2b" "2c" "3a" "3b" "3c")
Tampoco hay interrupción o continúa saliendo prematuramente.
Uso excesivo de estructuras
Vengo de un pasado OOPish así que cuando comencé Clojure mi cerebro aún pensaba en términos de objetos. Me encontré modelando todo como una estructura porque su agrupación de "miembros", por más suelta que sea, me hizo sentir cómodo. En realidad, las estructuras deberían considerarse principalmente como una optimización; Clojure compartirá las claves y cierta información de búsqueda para conservar memoria. Puede optimizarlos aún más definiendo accessors para acelerar el proceso de búsqueda de claves.
En general, no se gana nada con el uso de una estructura sobre un mapa, excepto por el rendimiento, por lo que la complejidad adicional podría no valer la pena.
Usar constructores BigDecimal no apagados
Necesitaba muchos BigDecimals y estaba escribiendo un código feo como este:
(let [foo (BigDecimal. "1") bar (BigDecimal. "42.42") baz (BigDecimal. "24.24")]
cuando, de hecho, Clojure admite literales BigDecimal añadiendo M al número:
(= (BigDecimal. "42.42") 42.42M) ; true
El uso de la versión azucarada elimina gran parte de la hinchazón. En los comentarios, twils mencionó que también puede usar las funciones bigdec y bigint para que sean más explícitas, pero que permanezcan concisas.
Usar el paquete Java para dar nombre a conversiones para espacios de nombres
Esto no es realmente un error per se, sino más bien algo que va en contra de la estructura idiomática y el nombramiento de un proyecto típico de Clojure. Mi primer proyecto sustancial de Clojure tenía declaraciones de espacios de nombres y estructuras de carpetas correspondientes, como esta:
(ns com.14clouds.myapp.repository)
que hinchó mis referencias de funciones completamente calificadas:
(com.14clouds.myapp.repository/load-by-name "foo")
Para complicar aún más las cosas, utilicé una estructura de directorio Maven estándar:
|-- src/
| |-- main/
| | |-- java/
| | |-- clojure/
| | |-- resources/
| |-- test/
...
que es más complejo que la estructura Clojure "estándar" de:
|-- src/
|-- test/
|-- resources/
que es el predeterminado de los proyectos de Leiningen y Clojure sí.
Los mapas utilizan Java''s Equals () en lugar de Clojure''s = para la coincidencia de claves
Originalmente reportado por chouser en IRC , este uso de los iguales de Java () conduce a algunos resultados poco intuitivos:
user> (= (int 1) (long 1))
true
user> ({(int 1) :found} (int 1) :not-found)
:found
user> ({(int 1) :found} (long 1) :not-found)
:not-found
Dado que tanto las instancias Integer como Long de 1 se imprimen de la misma manera por defecto, puede ser difícil detectar por qué su mapa no devuelve ningún valor. Esto es especialmente cierto cuando pasa su clave a través de una función que, quizás sin su conocimiento, devuelve una larga.
Se debe tener en cuenta que usar los valores iguales de Java () en lugar de Clojure = es esencial para que los mapas se ajusten a la interfaz java.util.Map.
Estoy usando Programming Clojure de Stuart Halloway, Practical Clojure de Luke VanderHart, y la ayuda de innumerables hackers de Clojure en IRC y la lista de correo para ayudar con mis respuestas.