Errores de programación comunes que deben evitar los desarrolladores de Clojure [cerrado]

92

¿Cuáles son algunos de los errores comunes que cometen los desarrolladores de Clojure y cómo podemos evitarlos?

Por ejemplo; los recién llegados a Clojure piensan que la contains?función funciona igual que java.util.Collection#contains. Sin embargo, contains?solo funcionará de manera similar cuando se use 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 usa con colecciones indexadas numéricamente (vectores, matrices) contains? solo verifica que el elemento dado esté dentro del rango válido de índices (basados ​​en cero):

(contains? [1 2 3 4] 4)
;=> false
(contains? [1 2 3 4] 0)
;=> true

Si se le da una lista, contains?nunca devolverá verdadero.

fogus
fuente
4
Solo para su información, para aquellos desarrolladores de Clojure que buscan java.util.Collection # contiene funcionalidad de tipo, consulte clojure.contrib.seq-utils / includes? De los documentos: Uso: (incluye? Coll x). Devuelve verdadero si coll contiene algo igual (con =) ax, en tiempo lineal.
Robert Campbell
11
Parece que han perdido el hecho de que esas preguntas son Wiki de la Comunidad
3
Me encanta cómo la pregunta de Perl tiene que estar fuera de sintonía con todas las demás :)
Éter
8
Para los desarrolladores de Clojure que busquen contenidos, recomendaría no seguir los consejos de rcampbell. seq-utils hace tiempo que está en desuso y esa función nunca fue útil para empezar. Puedes usar la somefunción de Clojure o, mejor aún, usarlo containssolo. Implementación de colecciones de Clojure java.util.Collection. (.contains [1 2 3] 2) => true
Rayne

Respuestas:

70

Octals literales

En un momento, estaba leyendo en una matriz que usaba ceros a la izquierda para mantener filas y columnas adecuadas. Matemáticamente, esto es correcto, ya que el cero a la izquierda 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 que me desconcertó totalmente. La razón es que Clojure trata los valores enteros literales con ceros a la izquierda como octales, y no hay un número 08 en octal.

También debo mencionar que Clojure admite valores hexadecimales tradicionales de Java a través del prefijo 0x . También puede utilizar cualquier base entre 2 y 36 utilizando la notación "base + r + valor", como 2r101010 o 36r16, que son 42 base diez.


Intentando devolver literales en una función literal anónima

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 entre paréntesis. Dado que es el primer elemento, se trata como una función (que en realidad es un mapa literal), pero no se proporcionan argumentos obligatorios (como una clave). En resumen, la función literal anónima no se expande a

(fn [%1 %2] {%1 %2})  ; notice the lack of parenthesis

por lo que no puede tener ningún valor literal ([],: a, 4,%) como cuerpo de la función anónima.

Se han dado dos soluciones en los comentarios. Brian Carper sugiere usar constructores de implementación de secuencia (array-map, hash-set, vector) así:

(#(array-map %1 %2) :a 1)

mientras que Dan muestra que puede usar la función de identidad para desenvolver el paréntesis externo:

(#(identity {%1 %2}) :a 1)

La sugerencia de Brian en realidad me lleva a mi próximo error ...


Pensando que el mapa hash o el mapa de matriz determinan la implementación del mapa concreto invariable

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

Si bien 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 bucle 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, de hecho, el bucle habría 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 predeterminado" (p3 775147 600851475143 3) con un bucle + enlace inicial. El repiten ahora vuelve a vincular los enlaces de bucle (en lugar de los parámetros fn) y salta de nuevo al punto de recursión (circular, en lugar de fn).


Haciendo referencia a vars "fantasmas"

Estoy hablando del tipo de var que podría definir usando el REPL, durante su programación exploratoria, y luego, sin saberlo, hacer referencia en su fuente. Todo funciona bien hasta que recarga el espacio de nombres (tal vez cerrando su editor) y luego descubre un montón de símbolos no vinculados a los que se hace referencia en todo su código. Esto también sucede con frecuencia cuando está refactorizando, moviendo una var de un espacio de nombres a otro.


Tratar la comprensión de la lista for como un imperativo bucle for

Básicamente, está creando una lista diferida basada en listas existentes en lugar de simplemente realizar un ciclo controlado. El doseq de Clojure es en realidad más análogo a las construcciones imperativas de bucle foreach.

Un ejemplo de cómo son diferentes es la capacidad de filtrar sobre qué elementos 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 de enlace, iterando primero sobre la expresión más a la derecha y trabajando hacia 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 continuar saliendo prematuramente.


Uso excesivo de estructuras

Vengo de un entorno OOPish, así que cuando comencé Clojure, mi cerebro todavía estaba pensando en términos de objetos. Me encontré modelando todo como una estructura porque su agrupación de "miembros", por más suelta que fuera, me hacía sentir cómodo. En realidad, las estructuras deben considerarse principalmente una optimización; Clojure compartirá las claves y cierta información de búsqueda para conservar la memoria. Puede optimizarlos aún más mediante la definición de accesos 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 el rendimiento, por lo que la complejidad adicional podría no valer la pena.


Usando constructores BigDecimal sin azúcar

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 agregando 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 ser más explícito, pero permanecer conciso.


Uso de las conversiones de nombres de paquetes de Java para espacios de nombres

En realidad, esto no es un error per se, sino algo que va en contra de la estructura idiomática y el nombre de un proyecto típico de Clojure. Mi primer proyecto sustancial de Clojure tenía declaraciones de espacio de nombres, y estructuras de carpetas correspondientes, como esta:

(ns com.14clouds.myapp.repository)

que infló mis referencias de funciones totalmente calificadas:

(com.14clouds.myapp.repository/load-by-name "foo")

Para complicar aún más las cosas, utilicé una estructura de directorio estándar de Maven :

|-- src/
|   |-- main/
|   |   |-- java/
|   |   |-- clojure/
|   |   |-- resources/
|   |-- test/
...

que es más compleja que la estructura "estándar" de Clojure de:

|-- src/
|-- test/
|-- resources/

que es el valor predeterminado de los proyectos de Leiningen y Clojure en sí.


Los mapas utilizan equals () de Java en lugar de Clojure = para la coincidencia de claves

Originalmente informado por Chouser en IRC , este uso de equals () 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 las instancias Integer y Long de 1 se imprimen de la misma manera de forma predeterminada, 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 que usted lo sepa, devuelve un long.

Cabe señalar que el uso de equals () 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 piratas informáticos de Clojure en IRC y la lista de correo para ayudar con mis respuestas.

rcampbell
fuente
1
Todas las macros del lector tienen una versión de función normal. Podrías hacer (#(hash-set %1 %2) :a 1)o en este caso (hash-set :a 1).
Brian Carper
2
También puede 'eliminar' los paréntesis adicionales con la identidad: (# (identity {% 1% 2}): a 1)
1
También es posible usar do: (#(do {%1 %2}) :a 1).
Michał Marczyk
@ Michał - No me gusta esta solución tanto como los anteriores, porque Do implica que un efecto secundario se lleva a cabo, cuando en realidad este no es el caso aquí.
Robert Campbell
@ rrc7cz: Bueno, en realidad, no hay necesidad de usar una función anónima aquí en absoluto, ya que usar hash-mapdirectamente (como en (hash-map :a 1)o (map hash-map keys vals)) es más legible y no implica que algo especial y que aún no se haya implementado en una función con nombre está teniendo lugar (lo que el uso de #(...)sí implica, creo). De hecho, el uso excesivo de fns anónimos es un problema en sí mismo. :-) OTOH, a veces utilizo dofunciones anónimas superconcisas que no tienen efectos secundarios ... Suele ser obvio que son de un solo vistazo. Es cuestión de gustos, supongo.
Michał Marczyk
42

Olvidar forzar la evaluación de secuencias perezosas

Las secuencias perezosas no se evalúan a menos que usted les pida que sean evaluadas. Puede esperar que esto imprima algo, pero no es así.

user=> (defn foo [] (map println [:foo :bar]) nil)
#'user/foo
user=> (foo)
nil

El mapno se evalúa, se desecha en silencio, porque es perezoso. Usted tiene que utilizar uno de doseq, dorun, doalletc, para forzar la evaluación de las secuencias de descanso para los efectos secundarios.

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

El uso de un bare mapen el tipo de REPL parece que funciona, pero solo funciona porque el REPL fuerza la evaluación de las secuencias perezosas. Esto puede hacer que el error sea aún más difícil de notar, 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)
Brian Carper
fuente
1
+1. Esto me mordió, pero de una manera más insidiosa: estaba evaluando (map ...)desde adentro (binding ...)y me preguntaba por qué no se aplican nuevos valores vinculantes.
Alex B
20

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 perezosas, pero con fines de depuración inserté algunas llamadas print / prn / pr, habiendo olvidado temporalmente qué era lo que estaba imprimiendo. Es curioso, ¿por qué mi PC está colgada?

tratando de programar Clojure imperativamente.

Hay una cierta tentación de crear una gran cantidad de refs o atoms y escribir código que constantemente Mucks 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 varios núcleos.

tratando de programar Clojure 100% funcionalmente.

Una otra cara de la moneda: algunos algoritmos realmente quieren un poco de estado mutable. Evitar religiosamente el estado mutable a toda costa puede resultar en algoritmos lentos o incómodos. Se necesita juicio y un poco de experiencia para tomar una 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 contenedor de lenguaje de scripting alrededor de Java. Ciertamente, necesitará hacer exactamente esto cuando use la funcionalidad de la biblioteca de Java, pero tiene poco sentido (por ejemplo) mantener estructuras de datos en Java, o usar tipos de datos de Java, como colecciones para las que hay buenos equivalentes en Clojure.

Carl Smotricz
fuente
13

Muchas cosas ya mencionadas. Solo agregaré uno más.

Clojure si trata los objetos booleanos de Java siempre como verdaderos, incluso si su valor es falso. Entonces, si tiene una función de tierra de Java que devuelve un valor booleano de Java, asegúrese de no verificarlo directamente, (if java-bool "Yes" "No") sino más bien (if (boolean java-bool) "Yes" "No").

Esto me quemó con la biblioteca clojure.contrib.sql que devuelve campos booleanos de base de datos como objetos booleanos java.

Vagif Verdi
fuente
8
Tenga en cuenta que (if java.lang.Boolean/FALSE (println "foo"))no imprime foo. (if (java.lang.Boolean. "false") (println "foo"))sin embargo, lo hace, mientras (if (boolean (java.lang.Boolean "false")) (println "foo"))que no ... ¡Muy confuso!
Michał Marczyk
Parece funcionar como se esperaba en Clojure 1.4.0: (assert (=: falso (si booleano / FALSO: verdadero: falso)))
Jakub Holý
También me quemé con este recientemente al hacer (filter: mykey coll) donde: los valores de mykey donde los booleanos: funcionan como se esperaba con las colecciones creadas por Clojure, pero NO con las colecciones deserializadas, cuando se serializan usando la serialización predeterminada de Java, porque esos booleanos están deserializados como nuevo booleano (), y lamentablemente (nuevo booleano (verdadero)! = java.lang.Boolean / TRUE)
Hendekagon
1
Solo recuerde las reglas básicas de los valores booleanos en Clojure, nily falseson falsas, y todo lo demás es cierto. Un Java Booleanno nillo es y no lo es false(porque es un objeto), por lo que el comportamiento es consistente.
erikprice
13

Manteniendo tu cabeza en bucles.
Corre el riesgo de quedarse sin memoria si recorre los elementos de una secuencia perezosa potencialmente muy grande o infinita mientras mantiene una referencia al primer elemento.

Olvidar que no hay TCO.
Las llamadas finales regulares consumen espacio de pila y se desbordarán si no tiene cuidado. Clojure tiene 'recury 'trampolinepara manejar muchos de los casos en los que las llamadas finales optimizadas se usarían en otros lenguajes, pero estas técnicas deben aplicarse intencionalmente.

Secuencias no del todo perezosas.
Puede crear una secuencia perezosa con 'lazy-seqo 'lazy-cons(o basándose en API perezosas de nivel superior), pero si la ajusta 'veco la pasa a través de alguna otra función que realice la secuencia, ya no será perezosa. Esto puede desbordar tanto la pila como el montón.

Poner cosas mutables en refs.
Técnicamente, puede hacerlo, pero solo la referencia del objeto en la propia referencia se rige por el STM, no el objeto referido y sus campos (a menos que sean inmutables y apunten a otras referencias). Entonces, siempre que sea posible, prefiera solo los objetos inmutables en las referencias. Lo mismo ocurre con los átomos.

Chris chaleco
fuente
4
la próxima rama de desarrollo contribuye en gran medida a reducir el primer elemento al borrar referencias a objetos en una función una vez que se vuelven localmente inaccesibles.
Arthur Ulfeldt
9

usar loop ... recurpara procesar secuencias cuando el mapa sea suficiente.

(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 Hotspot JIT generalmente la tiene optimizada y lista para funcionar sin ningún "tiempo de calentamiento".

Arthur Ulfeldt
fuente
1
Estas dos versiones en realidad no son equivalentes. Tu workfunción es equivalente a (doseq [item data] (do-stuff item)). (Además del hecho, ese ciclo en el trabajo nunca termina.)
kotarak
sí, el primero rompe con la pereza en sus argumentos. la secuencia resultante tendrá los mismos valores aunque ya no será una secuencia diferida.
Arthur Ulfeldt
+1! Escribí numerosas funciones recursivas pequeñas solo para encontrar otro día en el que todas estas podrían generalizarse usando mapy / o reduce.
nperson325681
5

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 entiendo del todo). Específicamente, las cadenas no son lo mismo que las secuencias de caracteres, aunque las funciones de secuencia funcionan en ellas:

user=> (filter #(> (int %) 96) "abcdABCDefghEFGH")
(\a \b \c \d \e \f \g \h)

Para volver a sacar una cuerda, debe hacer:

user=> (apply str (filter #(> (int %) 96) "abcdABCDefghEFGH"))
"abcdefgh"
Matt Fenwick
fuente
3

demasiados paréntesis, especialmente con la llamada al método java vacío dentro que da como resultado NPE:

public void foo() {}

((.foo))

da como resultado NPE de paréntesis externas porque las paréntesis internas se evalúan como nulo.

public int bar() { return 5; }

((.bar)) 

resulta en el más fácil de depurar:

java.lang.Integer cannot be cast to clojure.lang.IFn
  [Thrown class java.lang.ClassCastException]
miaubiz
fuente