24

与我使用的所有其他编程语言相比,我发现调试代码中的 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但这有一些问题

  1. :pre关于/种类的报道:post很糟糕。它只会从字面上打印出您测试的内容。因此,除非您付出很多努力,否则错误消息将无济于事。
  2. 这感觉不是很地道。我见过的唯一使用 / 的代码:pre:post解释如何使用 / 的:pre文章:post
  3. 将线程宏的步骤拉到他们自己defn的 s 中,以便我可以将:pre/:post放入其中,这真的很痛苦。
  4. 如果我虔诚地遵循这种做法,我认为我的代码可能会变得像 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}")))

这只修复了第一个要点。

我觉得要么

  1. 我有一个非常非常错误的开发过程,我不知道
  2. 那里有一些调试工具/库,我不知道其他人都这样做
  3. 其他人都遇到这样的问题,这是 Clojure 的肮脏小秘密/其他人都习惯于动态语言,并希望通过与我一样的努力来解决错误
  4. CounterClockWise 有一些错误让我的生活变得比需要的更艰难
  5. 我应该为我的 Clojure 代码编写比为我的 Java 代码编写更多的单元测试。即使我正在编写一次性代码。
4

4 回答 4

5

在这种特殊情况下,发现问题的根源很容易:

  1. 我们有一个函数可以应用于已知的项目向量。我们也期待一个特定的结果。

  2. 应用该功能会导致问题。那么让我们看看函数内部;它恰好是一个->>管道。

  3. 诊断问题最直接的方法是省略流水线的一些最后阶段,看看转换中的中间阶段是否如我们预期的那样。

做 3. 在 REPL 中特别简单;一种方法是def将输入和中间结果转换为临时变量,另一种方法是使用*1,*2*3. (如果管道很长或计算需要很长时间,我建议def至少每隔几步执行一次临时操作,否则*ns 可能就足够了。)

在其他情况下,您会做一些稍微不同的事情,但无论如何将工作分解为可管理的块以便在 REPL 中使用是关键。当然,熟悉 Clojure 的序列和集合库会大大加快这个过程。但是在你正在处理的实际任务的一小部分背景下与它们一起玩是了解它们的更好方法之一。

于 2013-06-03T17:21:21.613 回答
3

到目前为止,了解 clojure 异常的最佳方法(直到我们在 clojure 中可能有 clojure)是了解 clojure 是使用 Java 类、接口等实现的。因此,每当您遇到任何此类异常时,请尝试映射提到的类/接口在 clojure 概念的例外中。

例如:在您当前的异常中,可以很容易地推断出clojure.lang.PersistentVector正在尝试java.lang.Number在 method中键入 cast to clojure.lang.Numbers.add。从这些信息中,您可以查看您的代码并直观地找出您在代码中使用addie+的位置,然后通过这个 + 以某种方式将向量作为参数而不是数字的事实来诊断该问题。

于 2013-06-04T04:30:01.730 回答
2

我发现 clojure.tools.logging/spy 宏对于调试非常有用。它打印出包装的表达式及其值。如果设置 clojure.tools.logging 不是您现在想要做的事情(它需要正常的 Java 日志配置),您可以使用这个:

(defmacro spy
  [& body]
  `(let [x# ~@body]
     (printf "=> %s = %s\n" (first '~body) x#)
     x#))

*请记住,如果尚未实现,上面的代码不会打印出惰性序列的值。您可以vec一个惰性序列来强制其实现 - 不推荐用于无限序列。

不幸的是,我还没有找到在线程宏中使用 spy 宏的好方法,但对于大多数其他情况来说应该足够了。

于 2013-06-04T08:12:05.073 回答
1

Dynalint可能值得研究。它使用额外的检查来包装函数调用,这些检查会损害性能,但会提供更好的错误消息。

貌似不是一个很成熟的项目,一年没更新了,但是在错误信息的改进上已经有了一些进展。此外,它还在可能的GSoC 2015 项目列表中,因此我们可能很快就会看到很大的改进!

于 2015-02-23T09:18:12.807 回答