Как получить лучшую обратную связь от ошибок Clojure?
Мне очень сложно отлаживать ошибки Clojure, которые у меня есть в моем коде, по сравнению со всеми другими языками программирования, которые я использовал. Мой основной язык программирования - Java, и я очень новичок в Clojure. Большая часть моего времени написания Clojure проводится, пытаясь выяснить: "Почему я получаю эту ошибку?" и я хотел бы изменить это. Я использую CounterClockWise как свою первую IDE. Я не знаю, как использовать Emacs (пока?).
Вот пример:
(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 + %))))
Здесь я ошибочно считал, что group-by
возвращает список списков, но он фактически возвращает карту <key, list<value>>
или, как бы вы ее не выражали в терминах Java. Появится сообщение об ошибке:
ClassCastException Clojure.lang.PersistentVector нельзя передать в java.lang.Number Clojure.lang.Numbers.add(Numbers.java:126)
Это не очень полезно, потому что нет трассировки стека. Если я набираю (e)
, он говорит:
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)
Я понятия не имею, как я могу перейти от этого сообщения об ошибке к пониманию: "Вы считали, что проходили список списков в map
, но вы действительно проходили в виде данных карты". Трассировка стека показывает, что проблема была обнаружена внутри reduce
, а не внутри group-by
, но IMO, это не то место, где я, как человек, сделал мою ошибку. То, что программа обнаружила ошибку, была сделана.
Проблемы, подобные этим, могут занять около 15 минут. Как я могу сделать это меньше времени?
Я слишком много знаю, чтобы ожидать, что динамический язык поймает эти ошибки. Но я чувствую, что сообщения об ошибках других динамических языков, таких как javascript, гораздо полезнее.
Я получаю отчаяние здесь, потому что я читал код Clojure в течение 1-2 месяцев, и мне кажется, что я должен лучше разбираться в этих проблемах. Я попытался использовать :pre
/:post
для функций, но у него есть некоторые проблемы
- Отчеты о
:pre
/:post
отстой. Он только печатает буквально то, что вы тестируете. Поэтому, если вы не приложите много усилий, сообщение об ошибке не поможет.
- Это не очень идиоматично. Единственный код, который я видел, который использует
:pre
/:post
, - это статьи, в которых объясняется, как использовать :pre
/:post
.
- Это настоящая боль, чтобы вытащить шаги потокового макроса в их собственный
defn
, чтобы я мог поместить в них :pre
/:post
.
- Если бы я придерживался этой практики религиозно, я думаю, что мой код может стать столь же многословным, как Java. Я буду изобретать систему типов вручную.
Я дошел до того места, где я перекачал свой код с помощью проверок безопасности:
(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}")))
Которая только фиксирует эту первую маркерную точку.
Я чувствую себя как
- У меня есть процесс разработки, который очень, очень неправильный, и я не знаю его
- Там есть какой-то инструмент/библиотека отладки, который я не знаю о том, что все остальные
- У всех остальных есть такие проблемы, и это Clojure грязный маленький секрет/Все остальные используются для динамических языков и ожидают, что пройдут те же усилия, что и я, чтобы разрешить ошибки.
- CounterClockWise имеет некоторую ошибку, которая делает мою жизнь более сложной, чем она должна быть
- Я должен писать намного больше модульных тестов для моего кода Clojure, чем для моего кода Java. Даже если я пишу код.
Ответы
Ответ 1
В этом конкретном случае обнаружить источник проблемы легко:
-
У нас есть функция, которая будет применяться к известному вектору элементов. Мы также ожидаем определенного результата.
-
Применение функции приводит к проблеме. Тогда загляните внутрь функции; он является конвейером ->>
.
-
Самый простой способ диагностики проблемы - оставить некоторые из заключительных этапов конвейера, чтобы увидеть, будут ли промежуточные этапы преобразования такими, какие мы ожидаем.
Выполнение 3. особенно очевидно в REPL; один подход заключается в def
входном и промежуточном результатах на временные Vars, другой - на использование *1
, *2
и *3
. (Если конвейер длинный или вычисления занимают много времени, я бы рекомендовал сделать временный def
не реже одного раза в несколько шагов, иначе *n
может быть достаточно.)
В других случаях вы делаете что-то немного другое, но в любом случае разбить работу на управляемые фрагменты, которые будут воспроизводиться с помощью REPL, это ключ. Разумеется, знакомство с библиотеками последовательностей и коллекций Clojure ускоряет процесс совсем немного; но затем играть с ними в контексте небольших фрагментов фактической задачи, над которой вы работаете, является одним из лучших способов узнать о них.
Ответ 2
Лучший способ понять исключения clojure на данный момент (пока, вероятно, у нас есть clojure в clojure), следует понимать, что clojure реализуется с использованием классов Java, интерфейсов и т.д. Поэтому всякий раз, когда вы получаете любое такое исключение пытается сопоставить классы/интерфейсы, упомянутые в исключении, с понятиями clojure.
Например: в вашем текущем исключении можно легко сделать вывод о том, что clojure.lang.PersistentVector
пытались ввести cast в java.lang.Number
в методе clojure.lang.Numbers.add
. Из этой информации вы можете просмотреть свой код и интуитивно выяснить, где вы используете add
i.e +
в своем коде, а затем диагностировать эту проблему из-за того, что каким-то образом это + получает вектор как параметр вместо числа.
Ответ 3
Я нахожу, что макрос clojure.tools.logging/spy очень полезен для отладки. Он печатает завернутое выражение, а также его значение. Если настройка clojure.tools.logging - это не то, что вы хотите сделать прямо сейчас (для этого требуются обычные настройки ведения журнала Java), вы можете использовать это:
(defmacro spy
[& body]
`(let [x# [email protected]]
(printf "=> %s = %s\n" (first '~body) x#)
x#))
* имейте в виду, что приведенный выше код не печатает значения ленивого seq, если он не был реализован. Вы можете vec lazy seq, чтобы заставить его реализовать - не рекомендуется для бесконечных seqs.
К сожалению, я не нашел хороший способ использовать макрос шпиона в макросе потоковой передачи, но этого достаточно для большинства других случаев.
Ответ 4
Dynalint может стоить посмотреть. Он переносит вызовы функций с дополнительными проверками, которые ухудшают производительность, но обеспечивают лучшие сообщения об ошибках.
Это не очень зрелый проект и не обновлялся в течение года, но он уже успевает улучшить сообщения об ошибках. Кроме того, он находится в списке возможных GSoC 2015, поэтому мы можем увидеть значительное улучшение в ближайшее время!