Узнайте, где произошла ошибка в Clojure



По большей части я понимаю, что Clojure говорит мне с его сообщениями об ошибках. Но я все еще не соображаете, как выяснить, где произошла ошибка.



Вот пример того, что я имею в виду



(defn extract [m]
(keys m))

(defn multiple [xs]
(map #(* 2 %) xs))

(defn process [xs]
(-> xs
(multiple) ; seq -> seq
(extract))) ; map -> seq ... fails

(process [1 2 3])


Статически типизированные языки теперь сказали бы мне, что я пытался передать последовательность функции, которая ожидает отображение на строке X. И Clojure делает это таким образом:



ClassCastException java.lang.Long cannot be cast to java.util.Map$Entry


Но я до сих пор не знаю, где произошла ошибка. Очевидно, для этого примера это легко, потому что есть только 3 функции, которые участвуют, вы можете легко прочитать все из них, но по мере того, как программы растут больше, это становится старым очень быстро.

Есть ли способ узнать, где произошли ошибки, кроме простого корректного чтения кода сверху вниз? (это мой текущий подход)

570   3  

3 ответов:

Вы можете использовать clojure.спекуляция. Он все еще находится в alpha, и все еще есть куча поддержки инструментов (надеюсь), но функции инструментирования работают хорошо.

(ns foo.core
  (:require
   ;; For clojure 1.9.0-alpha16 and higher, it is called spec.alpha
   [clojure.spec.alpha :as s]
   [clojure.spec.test.alpha :as stest]))


;; Extract takes a map and returns a seq
(s/fdef extract
  :args (s/cat :m map?)
  :ret seq?)

(defn extract [m]
  (keys m))


;; multiple takes a coll of numbers and returns a coll of numbers
(s/fdef multiple
  :args (s/cat :xs (s/coll-of number?))
  :ret (s/coll-of number?))

(defn multiple [xs]
  (map #(* 2 %) xs))


(defn process [xs]
  (-> xs
      (multiple)     ; seq -> seq
      (extract))) ; map -> seq ... fails

;; This needs to come after the definition of the specs,
;; but before the call to process.
;; This is something I imagine can be handled automatically
;; by tooling at some point.
(stest/instrument)

;; The println is to force evaluation.
;; If not it wouldn't run because it's lazy and
;; not used for anything.
(println (process [1 2 3]))

Запуск этого файла печатает (среди другой информации):

Call to #'foo.core/extract did not conform to spec: In: [0] val: (2
4 6) fails at: [:args :m] predicate: map?  :clojure.spec.alpha/spec
#object[clojure.spec.alpha$regex_spec_impl$reify__1200 0x2b935f0d
"clojure.spec.alpha$regex_spec_impl$reify__1200@2b935f0d"]
:clojure.spec.alpha/value ((2 4 6)) :clojure.spec.alpha/args ((2 4
6)) :clojure.spec.alpha/failure :instrument
:clojure.spec.test.alpha/caller {:file "core.clj", :line 29,
:var-scope foo.core/process}

, который может быть прочитан как: вызов exctract не удался, потому что значение, переданное в (2 4 6), не выполнило предикат map?. Этот вызов произошел в файле "core.clj" в строке 29.

Предостережение, которое выводит людей из себя, заключается в том, что инструмент проверяет только аргументы функций, а не значение. Это (странное, если вы спросите меня) дизайнерское решение от Рича Хики. Хотя для этого есть библиотека .

Если у вас есть сеанс REPL, вы можете распечатать трассировку стека:

(clojure.stacktrace/print-stack-trace *e 30)

См. http://puredanger.github.io/tech.puredanger.com/2010/02/17/clojure-stack-trace-repl/ для различных способов печати трассировки стека. Вам нужно будет иметь такую зависимость в вашем project.clj:

[org.clojure/tools.namespace "0.2.11"]

Я не получил трассировку стека, используя вышеуказанный метод, однако просто набрав *e в REPL, вы получите всю доступную информацию об ошибке, которая, честно говоря, не показалась вам очень полезно.

В редких случаях, когда трассировка стека не помогает, я обычно отлаживаю с помощью вызова функции, которая возвращает единственный аргумент, который ей задан, но имеет побочный эффект печати этого аргумента. Я случайно вызываю эту функцию probe. В вашем случае он может быть помещен в несколько мест в макро-потоке.

Перепечатка вашего примера у меня есть:

(defn extract [m]
  (keys m))

(defn multiply [xs]
  (mapv #(* 2 %) xs))

(defn process [xs]
  (-> xs
    (multiply)      ; seq -> seq
    (extract)))     ; map -> seq ... fails ***line 21***

(println (process [1 2 3]))
;=> java.lang.ClassCastException: java.lang.Long cannot be cast 
to java.util.Map$Entry, compiling:(tst/clj/core.clj:21:21)

Таким образом, мы получаем хороший ключ в исключении, где is говорит файлу и номеру строки/col tst.clj.core.clj:21:21, что метод extract является проблемой.

Другим незаменимым инструментом, который я использую, является Plumatic Schema для внедрения "постепенной" проверки типа в clojure. Код становится:
(ns tst.clj.core
  (:use clj.core tupelo.test)
  (:require
    [tupelo.core :as t]
    [tupelo.schema :as tsk]
    [schema.core :as s]))
(t/refer-tupelo)
(t/print-versions)

(s/defn extract :- [s/Any]
  [m :- tsk/Map]
  (keys m))

(s/defn multiply :- [s/Num]
  [xs :- [s/Num]]
  (mapv #(* 2 %) xs))

(s/defn process :- s/Any
  [xs :- [s/Num]]
  (-> xs
    (multiply) ; seq -> seq
    (extract))) ; map -> seq ... fails

(println (process [1 2 3])) 

clojure.lang.ExceptionInfo: Input to extract does not match schema: 
[(named (not (map? [2 4 6])) m)] {:type :schema.core/error, :schema [#schema.core.One{:schema {Any Any}, 
:optional? false, :name m}], 
:value [[2 4 6]], :error [(named (not (map? [2 4 6])) m)]}, 
compiling:(tst/clj/core.clj:23:17)

Таким образом, хотя формат сообщения об ошибке немного длинноват, он сразу говорит, что мы передали в метод параметр неправильного типа и/или формы extract.

Обратите внимание, что вам нужна такая строка:

(s/set-fn-validation! true) ; enforce fn schemas

Я создаю специальный файл test/tst/clj/_bootstrap.clj, так что он всегда находится в одном и том же месте.

Для получения дополнительной информации о схеме Plumatic, пожалуйста смотрите:

Comments

    Ничего не найдено.