¿Cómo obtengo mejores comentarios de los errores de Clojure?
runtime-error (4)
En este caso particular, descubrir la fuente del problema es fácil:
Tenemos una función que se aplicará a un vector conocido de elementos. También estamos esperando un resultado particular.
La aplicación de los resultados de la función en un problema. Echemos un vistazo dentro de la función entonces; pasa a ser un
->>
tubería.La forma más sencilla de diagnosticar el problema es dejar de lado algunas de las etapas finales de la tubería para ver si las etapas intermedias en la transformación son las que esperamos.
Hacer 3. es particularmente sencillo en el REPL; un enfoque es def
la entrada y los resultados intermedios a Vars temporales, otro es usar *1
, *2
y *3
. (Si la tubería es larga o los cálculos toman mucho tiempo, recomendaría hacer una def
temporal al menos una vez cada pocos pasos, de lo contrario, la *n
s podría ser suficiente).
En otros casos, haría algo ligeramente diferente, pero en cualquier caso dividir el trabajo en trozos manejables para jugar en el REPL es la clave. Por supuesto, la familiaridad con las bibliotecas de secuencias y colecciones de Clojure acelera el proceso bastante; pero luego jugar con ellos en el contexto de pequeños fragmentos de una tarea real en la que estás trabajando es una de las mejores maneras de aprender sobre ellos.
Me resulta muy difícil depurar los errores de Clojure que tengo en mi código en comparación con todos los otros lenguajes de programación que he usado. Mi lenguaje de programación principal es Java y soy muy nuevo en Clojure. La mayor parte de mi tiempo escribiendo Clojure lo dedico a tratar de averiguar "¿Por qué recibo este error?" y me gustaría cambiar eso. Estoy usando CounterClockWise como mi IDE principal. No sé cómo usar Emacs (¿todavía?).
Aquí hay un ejemplo:
(ns cljsandbox.core)
(def l [1 2 3 1])
(defn foo
[l]
(->> l
(group-by identity)
;vals ;commented out to show my intent
(map #(reduce + %))))
Aquí, erróneamente pensé que group-by
devuelve una lista de listas, pero en realidad devuelve un mapa de <key, list<value>>
o como lo expresaría en términos de Java. Esto da un mensaje de error que dice:
ClassCastException clojure.lang.PersistentVector no se puede convertir a java.lang.Number clojure.lang.Numbers.add (Numbers.java:126)
Esto no es muy útil porque no hay un rastro de pila. Si escribo (e)
dice:
java.lang.ClassCastException: clojure.lang.PersistentVector cannot be cast to java.lang.Number
at clojure.lang.Numbers.add (Numbers.java:126)
clojure.core$_PLUS_.invoke (core.clj:944)
clojure.core.protocols/fn (protocols.clj:69)
clojure.core.protocols$fn__5979$G__5974__5992.invoke (protocols.clj:13)
clojure.core$reduce.invoke (core.clj:6175)
cljsandbox.core$foo$fn__1599.invoke (core.clj:10)
clojure.core$map$fn__4207.invoke (core.clj:2487)
clojure.lang.LazySeq.sval (LazySeq.java:42)
No tengo idea de cómo puedo pasar de este mensaje de error a la comprensión: "Pensaste que pasabas una lista de listas al map
pero realmente pasabas un tipo de datos del mapa". El seguimiento de la pila muestra que el problema se informó dentro de reduce
, no dentro de group-by
, sino en IMO, que no es donde yo como humano cometí mi error. Ahí es donde el programa descubrió que se había cometido un error.
Problemas como estos pueden tardar más de 15 minutos en resolverse. ¿Cómo puedo hacer que esto tome menos tiempo?
Sé que es demasiado esperar un lenguaje dinámico para detectar estos errores. Pero siento que los mensajes de error de otros lenguajes dinámicos como javascript son mucho más útiles.
Me estoy volviendo bastante desesperado aquí, porque he estado programando clojure durante 1-2 meses y siento que debería tener un mejor manejo para resolver estos problemas. Intenté usar :pre
/ :post
en funciones pero eso tiene algunos problemas
- La información sobre
:pre
/:post
tipo de chupa. Solo imprime literalmente lo que pruebas. Por lo tanto, a menos que ponga mucho esfuerzo en ello, el mensaje de error no es útil. - Esto no se siente muy idiomático. El único código que he visto que usa
:pre
/:post
son artículos que explican cómo usar:pre
/:post
. - Es un verdadero dolor sacar los pasos de una macro de subprocesamiento en sus propios
defn
para que pueda poner el:pre
/:post
en ellos. - Si seguí esta práctica religiosamente, creo que mi código podría llegar a ser tan detallado como Java. Estaría reinventando el sistema de tipos a mano.
He llegado al punto en el que salpico mi código con controles de seguridad como este:
(when (= next-url url)
(throw (IllegalStateException. (str "The next url and the current url are the same " url))))
(when-not (every? map? posts-list)
(throw (IllegalStateException. "parsed-html->posts must return a list of {:post post :source-url source-url}")))
Lo que solo corrige ese primer punto.
Me siento como tampoco
- Tengo un proceso de desarrollo que está muy, muy mal y no lo sé
- Hay alguna herramienta / biblioteca de depuración que no conozco que todos los demás saben
- Todos los demás tienen problemas como este y es el pequeño secreto de Clojure. Todos los demás están acostumbrados a los lenguajes dinámicos y esperan pasar por el mismo esfuerzo que yo para resolver errores.
- CounterClockWise tiene un error que hace mi vida mucho más difícil de lo que necesita ser
- Se supone que estoy escribiendo muchas más pruebas unitarias para mi código Clojure que para mi código Java. Incluso si estoy escribiendo código desechable.
Encuentro la macro clojure.tools.logging / spy muy útil para la depuración. Imprime la expresión envuelta, así como su valor. Si la configuración de clojure.tools.logging no es algo que desee hacer ahora (requiere las configuraciones normales de registro de Java), puede usar esto:
(defmacro spy
[& body]
`(let [x# ~@body]
(printf "=> %s = %s/n" (first ''~body) x#)
x#))
* tenga en cuenta que el código anterior no imprime los valores de una secuencia perezosa si no se ha realizado. Puede acercarse a una secuencia perezosa para forzar su realización; no se recomienda para secuencias infinitas.
Desafortunadamente, no he encontrado una buena manera de usar la macro espía dentro de una macro de subprocesos, pero debería ser suficiente para la mayoría de los otros casos.
La mejor manera de dar sentido a las excepciones de clojure a partir de ahora (hasta que probablemente tengamos clojure en clojure) es entender que clojure se implementa utilizando clases Java, interfaces, etc. Por lo tanto, cuando obtenga una excepción, intente asignar las clases / interfaces mencionadas En la excepción de los conceptos de clojure.
Por ejemplo: en su excepción actual, se puede inferir fácilmente que clojure.lang.PersistentVector
se intentó escribir cast para java.lang.Number
en el método clojure.lang.Numbers.add
. A partir de esta información, puede buscar en su código e imaginar de manera intuitiva dónde está utilizando add
ie +
en su código y luego diagnosticar ese problema por el hecho de que de alguna manera este + es vector como parámetro en lugar de número.
Dynalint podría valer la pena mirar. Envuelve las llamadas a funciones con controles adicionales que dañan el rendimiento, pero que brindan mejores mensajes de error.
No parece ser un proyecto muy maduro, y no se ha actualizado durante un año, pero ya ha progresado para mejorar los mensajes de error. Además, está en la lista de posibles proyectos GSoC 2015, por lo que podremos ver una gran mejora pronto.