¿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.
some
función de Clojure o, mejor aún, usarlocontains
solo. Implementación de colecciones de Clojurejava.util.Collection
.(.contains [1 2 3] 2) => true
Respuestas:
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:
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:
así que creí que esto también funcionaría:
pero falla con:
porque la macro del lector # () se expande a
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
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í:
mientras que Dan muestra que puede usar la función de identidad para desenvolver el paréntesis externo:
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:
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:
Cuando, de hecho, el bucle habría sido más conciso e idiomático para esta función en particular:
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:
Otra forma en que son diferentes es que pueden operar en infinitas secuencias perezosas:
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:
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:
cuando, de hecho, Clojure admite literales BigDecimal agregando M al número:
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:
que infló mis referencias de funciones totalmente calificadas:
Para complicar aún más las cosas, utilicé una estructura de directorio estándar de Maven :
que es más compleja que la estructura "estándar" de Clojure de:
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:
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.
fuente
(#(hash-set %1 %2) :a 1)
o en este caso(hash-set :a 1)
.do
:(#(do {%1 %2}) :a 1)
.hash-map
directamente (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 utilizodo
funciones anónimas superconcisas que no tienen efectos secundarios ... Suele ser obvio que son de un solo vistazo. Es cuestión de gustos, supongo.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í.
El
map
no se evalúa, se desecha en silencio, porque es perezoso. Usted tiene que utilizar uno dedoseq
,dorun
,doall
etc, para forzar la evaluación de las secuencias de descanso para los efectos secundarios.El uso de un bare
map
en 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.fuente
(map ...)
desde adentro(binding ...)
y me preguntaba por qué no se aplican nuevos valores vinculantes.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
ref
s oatom
s 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.
fuente
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.
fuente
(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!nil
yfalse
son falsas, y todo lo demás es cierto. Un JavaBoolean
nonil
lo es y no lo esfalse
(porque es un objeto), por lo que el comportamiento es consistente.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
'recur
y'trampoline
para 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-seq
o'lazy-cons
(o basándose en API perezosas de nivel superior), pero si la ajusta'vec
o 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.
fuente
usar
loop ... recur
para procesar secuencias cuando el mapa sea suficiente.vs.
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".
fuente
work
función es equivalente a(doseq [item data] (do-stuff item))
. (Además del hecho, ese ciclo en el trabajo nunca termina.)map
y / oreduce
.Los tipos de colección tienen comportamientos diferentes para algunas operaciones:
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:
Para volver a sacar una cuerda, debe hacer:
fuente
demasiados paréntesis, especialmente con la llamada al método java vacío dentro que da como resultado NPE:
da como resultado NPE de paréntesis externas porque las paréntesis internas se evalúan como nulo.
resulta en el más fácil de depurar:
fuente