为什么Clojure的堆栈跟踪(stacktrace)这么长?

33
我曾经编写过一些玩具解释器/编译器,因此将堆栈跟踪引用编译器源文件中的行与编译器错误相关联。
如果我向我的compojure项目添加以下错误的函数定义:
(defn dodgy-map []
  {:1 :2 :3})

Lein拒绝启动:

$ lein ring server-headless
Exception in thread "main" java.lang.RuntimeException: Map literal must contain an even number of forms, compiling:(one_man_wiki/views.clj:19)
        at clojure.lang.Compiler.load(Compiler.java:6958)
        at clojure.lang.RT.loadResourceScript(RT.java:359)
        at clojure.lang.RT.loadResourceScript(RT.java:350)
        at clojure.lang.RT.load(RT.java:429)
        at clojure.lang.RT.load(RT.java:400)
        at clojure.core$load$fn__4890.invoke(core.clj:5415)
        at clojure.core$load.doInvoke(core.clj:5414)
        at clojure.lang.RestFn.invoke(RestFn.java:408)
        at clojure.core$load_one.invoke(core.clj:5227)
        at clojure.core$load_lib.doInvoke(core.clj:5264)
        at clojure.lang.RestFn.applyTo(RestFn.java:142)
        at clojure.core$apply.invoke(core.clj:603)
        at clojure.core$load_libs.doInvoke(core.clj:5298)
        at clojure.lang.RestFn.applyTo(RestFn.java:137)
        at clojure.core$apply.invoke(core.clj:603)
        at clojure.core$require.doInvoke(core.clj:5381)
        at clojure.lang.RestFn.invoke(RestFn.java:457)
        at one_man_wiki.handler$eval1564$loading__4784__auto____1565.invoke(handler.clj:1)
        at one_man_wiki.handler$eval1564.invoke(handler.clj:1)
        at clojure.lang.Compiler.eval(Compiler.java:6511)
        at clojure.lang.Compiler.eval(Compiler.java:6501)
        at clojure.lang.Compiler.load(Compiler.java:6952)
        at clojure.lang.RT.loadResourceScript(RT.java:359)
        at clojure.lang.RT.loadResourceScript(RT.java:350)
        at clojure.lang.RT.load(RT.java:429)
        at clojure.lang.RT.load(RT.java:400)
        at clojure.core$load$fn__4890.invoke(core.clj:5415)
        at clojure.core$load.doInvoke(core.clj:5414)
        at clojure.lang.RestFn.invoke(RestFn.java:408)
        at clojure.core$load_one.invoke(core.clj:5227)
        at clojure.core$load_lib.doInvoke(core.clj:5264)
        at clojure.lang.RestFn.applyTo(RestFn.java:142)
        at clojure.core$apply.invoke(core.clj:603)
        at clojure.core$load_libs.doInvoke(core.clj:5298)
        at clojure.lang.RestFn.applyTo(RestFn.java:137)
        at clojure.core$apply.invoke(core.clj:603)
        at clojure.core$require.doInvoke(core.clj:5381)
        at clojure.lang.RestFn.invoke(RestFn.java:421)
        at user$eval1.invoke(NO_SOURCE_FILE:1)
        at clojure.lang.Compiler.eval(Compiler.java:6511)
        at clojure.lang.Compiler.eval(Compiler.java:6500)
        at clojure.lang.Compiler.eval(Compiler.java:6477)
        at clojure.core$eval.invoke(core.clj:2797)
        at clojure.main$eval_opt.invoke(main.clj:297)
        at clojure.main$initialize.invoke(main.clj:316)
        at clojure.main$null_opt.invoke(main.clj:349)
        at clojure.main$main.doInvoke(main.clj:427)
        at clojure.lang.RestFn.invoke(RestFn.java:421)
        at clojure.lang.Var.invoke(Var.java:419)
        at clojure.lang.AFn.applyToHelper(AFn.java:163)
        at clojure.lang.Var.applyTo(Var.java:532)
        at clojure.main.main(main.java:37)
Caused by: java.lang.RuntimeException: Map literal must contain an even number of forms
        at clojure.lang.Util.runtimeException(Util.java:170)
        at clojure.lang.LispReader$MapReader.invoke(LispReader.java:1071)
        at clojure.lang.LispReader.readDelimitedList(LispReader.java:1126)
        at clojure.lang.LispReader$ListReader.invoke(LispReader.java:962)
        at clojure.lang.LispReader.read(LispReader.java:180)
        at clojure.lang.Compiler.load(Compiler.java:6949)
        ... 51 more

如果我引用了一个不存在的变量:
(defn no-such-variable []
  i-dont-exist)

我得到了一个同样巨大的回溯:
Exception in thread "main" java.lang.RuntimeException: Unable to resolve symbol: i-dont-exist in this context, compiling:(one_man_wiki/views.clj:18)
        at clojure.lang.Compiler.analyze(Compiler.java:6281)
        at clojure.lang.Compiler.analyze(Compiler.java:6223)
        at clojure.lang.Compiler$BodyExpr$Parser.parse(Compiler.java:5618)
        at clojure.lang.Compiler$FnMethod.parse(Compiler.java:5054)
        at clojure.lang.Compiler$FnExpr.parse(Compiler.java:3674)
        at clojure.lang.Compiler.analyzeSeq(Compiler.java:6453)
        at clojure.lang.Compiler.analyze(Compiler.java:6262)
        at clojure.lang.Compiler.analyzeSeq(Compiler.java:6443)
        at clojure.lang.Compiler.analyze(Compiler.java:6262)
        at clojure.lang.Compiler.access$100(Compiler.java:37)
        at clojure.lang.Compiler$DefExpr$Parser.parse(Compiler.java:518)
        at clojure.lang.Compiler.analyzeSeq(Compiler.java:6455)
        at clojure.lang.Compiler.analyze(Compiler.java:6262)
        at clojure.lang.Compiler.analyze(Compiler.java:6223)
        at clojure.lang.Compiler.eval(Compiler.java:6515)
        at clojure.lang.Compiler.load(Compiler.java:6952)
        at clojure.lang.RT.loadResourceScript(RT.java:359)
        at clojure.lang.RT.loadResourceScript(RT.java:350)
        at clojure.lang.RT.load(RT.java:429)
        at clojure.lang.RT.load(RT.java:400)
        at clojure.core$load$fn__4890.invoke(core.clj:5415)
        at clojure.core$load.doInvoke(core.clj:5414)
        at clojure.lang.RestFn.invoke(RestFn.java:408)
        at clojure.core$load_one.invoke(core.clj:5227)
        at clojure.core$load_lib.doInvoke(core.clj:5264)
        at clojure.lang.RestFn.applyTo(RestFn.java:142)
        at clojure.core$apply.invoke(core.clj:603)
        at clojure.core$load_libs.doInvoke(core.clj:5298)
        at clojure.lang.RestFn.applyTo(RestFn.java:137)
        at clojure.core$apply.invoke(core.clj:603)
        at clojure.core$require.doInvoke(core.clj:5381)
        at clojure.lang.RestFn.invoke(RestFn.java:457)
        at one_man_wiki.handler$eval1564$loading__4784__auto____1565.invoke(handler.clj:1)
        at one_man_wiki.handler$eval1564.invoke(handler.clj:1)
        at clojure.lang.Compiler.eval(Compiler.java:6511)
        at clojure.lang.Compiler.eval(Compiler.java:6501)
        at clojure.lang.Compiler.load(Compiler.java:6952)
        at clojure.lang.RT.loadResourceScript(RT.java:359)
        at clojure.lang.RT.loadResourceScript(RT.java:350)
        at clojure.lang.RT.load(RT.java:429)
        at clojure.lang.RT.load(RT.java:400)
        at clojure.core$load$fn__4890.invoke(core.clj:5415)
        at clojure.core$load.doInvoke(core.clj:5414)
        at clojure.lang.RestFn.invoke(RestFn.java:408)
        at clojure.core$load_one.invoke(core.clj:5227)
        at clojure.core$load_lib.doInvoke(core.clj:5264)
        at clojure.lang.RestFn.applyTo(RestFn.java:142)
        at clojure.core$apply.invoke(core.clj:603)
        at clojure.core$load_libs.doInvoke(core.clj:5298)
        at clojure.lang.RestFn.applyTo(RestFn.java:137)
        at clojure.core$apply.invoke(core.clj:603)
        at clojure.core$require.doInvoke(core.clj:5381)
        at clojure.lang.RestFn.invoke(RestFn.java:421)
        at user$eval1.invoke(NO_SOURCE_FILE:1)
        at clojure.lang.Compiler.eval(Compiler.java:6511)
        at clojure.lang.Compiler.eval(Compiler.java:6500)
        at clojure.lang.Compiler.eval(Compiler.java:6477)
        at clojure.core$eval.invoke(core.clj:2797)
        at clojure.main$eval_opt.invoke(main.clj:297)
        at clojure.main$initialize.invoke(main.clj:316)
        at clojure.main$null_opt.invoke(main.clj:349)
        at clojure.main$main.doInvoke(main.clj:427)
        at clojure.lang.RestFn.invoke(RestFn.java:421)
        at clojure.lang.Var.invoke(Var.java:419)
        at clojure.lang.AFn.applyToHelper(AFn.java:163)
        at clojure.lang.Var.applyTo(Var.java:532)
        at clojure.main.main(main.java:37)
Caused by: java.lang.RuntimeException: Unable to resolve symbol: i-dont-exist in this context
        at clojure.lang.Util.runtimeException(Util.java:170)
        at clojure.lang.Compiler.resolveIn(Compiler.java:6766)
        at clojure.lang.Compiler.resolve(Compiler.java:6710)
        at clojure.lang.Compiler.analyzeSymbol(Compiler.java:6671)
        at clojure.lang.Compiler.analyze(Compiler.java:6244)
        ... 66 more

为什么Clojure编译器不会引发ClojureSyntaxErrorClojureNameError,以便在顶层捕获并显示简单的错误?这些是开发过程中常见的开发人员错误。

如果长的回溯在某些情况下有用,为什么它们被截断了?

编辑:我在答案中寻找的内容:

  1. 是否存在某些情况(例如与Java互操作的某些元编程)下,获取引用clojure.lang的回溯有用?
  2. (相关)是否有任何技术限制阻止添加我上述描述的ClojureSyntaxError?值得我在Clojure bug跟踪器上开启一个问题吗?(更新:我已经opened CLJ-1155
  3. 有经验的Clojure程序员如何阅读这些回溯?是否有工具可帮助?是否每个人都使用clj-stacktrace?

6
我同意,Clojure 在某些方面与 C++ 竞争。 - Ben
你能否编辑一下这个问题,更清晰地解释一个合适的答案应该是什么样子的? - Arthur Ulfeldt
2
mods - 虽然它肯定可以改进,但在我看来,这个问题是合理的,并且可以得到建设性的回答(正如JohnJ在下面展示的那样)。投票重新开放。 - mikera
1
+1 重新开放,注意到目前问题投票数量较高。 - JohnJ
2个回答

14

回答你的编号问题:

  1. Clojure 习惯于与 Java 库直接交互,因此在以某种意外或不受支持的方式调用 Java 对象时,获得完整的 Java 堆栈跟踪信息可能会有所帮助。

  2. 听起来是个不错的主意;我经常至少希望有一个可设置选项,允许我只查看源自我自己代码的堆栈跟踪部分,抑制所有底层语言层。

  3. 通常我只是费力地研究堆栈跟踪,找到我的程序出问题的那一行(并且我通常会测试每一分钟的变化,所以我对导致问题的变化有很好的了解)。我使用过的一些 Emacs 和 Eclipse 工具仅显示实际错误而不是整个堆栈跟踪;我通常发现这更有帮助。

    2012 年 Clojure/west 上,@chouser 做了一个关于堆栈跟踪结构的演讲,并介绍了一个看起来很有前途但似乎仍未见天日的工具。你可以在这里[PDF]找到这个演讲。

与 Python 相比,我觉得 Clojure 的堆栈跟踪不太友好,特别是对于初学者来说。这在一定程度上是由于该语言的 "hosted" 本质,尽管我认为可以在不增加不必要复杂性的情况下进行改进。


现在一些Clojure工具可以过滤堆栈跟踪信息,使其更加清晰易读,例如expectations测试库 - Wilfred Hughes
1
@WilfredHughes - 很好的观点;此外,Emacs中最新的CIDER集成提供了按类型(Clojure、Java、REPL等)选择性过滤堆栈帧的功能。 - JohnJ

2
Clojure堆栈跟踪很嘈杂,这是一个已经被听到的投诉。一些库提供了更好的堆栈跟踪,例如clj-stacktraceexpectations library

但是这也是Clojure本身正在改进的方面,例如最近的1.7版本。还可以查看clojure.stacktrace api

所以我敢打赌随着时间的推移,它只会变得更好。现在Clojure {Script}生态系统非常令人兴奋。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接