Clojure 如何编译 REPL 中 运行 的代码?
How does Clojure compile the code that's run in a REPL?
诚实的菜鸟问题。基于 Russ Olsen 的 Getting Clojure,我知道以下内容:
(1) Clojure代码在运行时间之前被编译成JVM字节码。
(2) Clojure 代码可以是 运行,在使用函数 (read) 和 (eval) 或等效函数的 REPL 中几乎可以即时得到反馈。
因此,Clojure 代码到 JVM 字节码的编译似乎必须在 REPL 期间的某个时刻发生,大概是在(读取)阶段或之后不久。
但这是一个模糊的心理画面,我想澄清一下。
例如,很高兴知道代码在 REPL 中何时实际编译,编译创建的数据如何存储在 RAM 中,然后 由 (eval) 访问,以及其间或之后发生的任何重要步骤。
换句话说,我想更详细地了解香肠是如何制作的:
Clojure 如何编译 REPL 中 运行 的代码?
(加分:这与 Clojure 从非 REPL 源(例如 Leiningen 项目)编译代码时所做的有何不同?)
reader 使用字符并生成 Clojure 数据结构(列表、向量、符号等)。读取阶段肯定对 JVM 字节码一无所知。这是 eval 阶段的一部分:编译器使用这些数据结构并生成 JVM 字节码。
当 运行 REPL 时,该字节码存储在 DynamicClassLoader 中 - 所有 JVM classes 必须由一些 ClassLoader 定义,而 DynamicClassLoader 是 Clojure 创建的允许定义 classes 从 Clojure 数据结构中飞速发展。
编译成 class 文件时,相同的字节码只是简单地写入磁盘的 .class 文件中,然后可能打包到 jar 中。
一个简短的测试揭示了答案:
(ns tst.demo.core
(:use tupelo.core tupelo.test))
(defmacro with-trace
[tag & forms]
(println "*** running macro *** tag: " tag)
`(do
(println ~tag :enter)
(let [result# (do ~@forms)]
(println ~tag :leave)
result#)))
(dotest
(newline)
(println :answer
(with-trace :add-5
(+ 2 3)))
(newline)
(println :using-eval
(eval
(quote (tst.demo.core/with-trace :the-ultimate-answer
(clojure.core/inc 41))))))
打印:
> lein clean ; lein test
*** running macro *** tag: :add-5
lein test _bootstrap
-------------------------------
Clojure 1.10.1 Java 14
-------------------------------
lein test tst.demo.core
:add-5 :enter
:add-5 :leave
:answer 5
*** running macro *** tag: :the-ultimate-answer
:the-ultimate-answer :enter
:the-ultimate-answer :leave
:using-eval 42
所以我们看到 eval
执行 "evaluate" 宏和函数。
更新
@amalloy 是正确的,我忘记引用我发送到 eval
的代码,因此宏 with-trace
是在编译时计算的,而不是 运行 时。在这个版本中,发送到 eval
的代码被引用,所以它只是一个包含 2 个符号、1 个关键字和一个整数的嵌套列表。现在,eval
看到宏,运行 生成实际代码,然后编译 & 运行 宏输出。
诚实的菜鸟问题。基于 Russ Olsen 的 Getting Clojure,我知道以下内容:
(1) Clojure代码在运行时间之前被编译成JVM字节码。
(2) Clojure 代码可以是 运行,在使用函数 (read) 和 (eval) 或等效函数的 REPL 中几乎可以即时得到反馈。
因此,Clojure 代码到 JVM 字节码的编译似乎必须在 REPL 期间的某个时刻发生,大概是在(读取)阶段或之后不久。
但这是一个模糊的心理画面,我想澄清一下。
例如,很高兴知道代码在 REPL 中何时实际编译,编译创建的数据如何存储在 RAM 中,然后 由 (eval) 访问,以及其间或之后发生的任何重要步骤。
换句话说,我想更详细地了解香肠是如何制作的:
Clojure 如何编译 REPL 中 运行 的代码?
(加分:这与 Clojure 从非 REPL 源(例如 Leiningen 项目)编译代码时所做的有何不同?)
reader 使用字符并生成 Clojure 数据结构(列表、向量、符号等)。读取阶段肯定对 JVM 字节码一无所知。这是 eval 阶段的一部分:编译器使用这些数据结构并生成 JVM 字节码。
当 运行 REPL 时,该字节码存储在 DynamicClassLoader 中 - 所有 JVM classes 必须由一些 ClassLoader 定义,而 DynamicClassLoader 是 Clojure 创建的允许定义 classes 从 Clojure 数据结构中飞速发展。
编译成 class 文件时,相同的字节码只是简单地写入磁盘的 .class 文件中,然后可能打包到 jar 中。
一个简短的测试揭示了答案:
(ns tst.demo.core
(:use tupelo.core tupelo.test))
(defmacro with-trace
[tag & forms]
(println "*** running macro *** tag: " tag)
`(do
(println ~tag :enter)
(let [result# (do ~@forms)]
(println ~tag :leave)
result#)))
(dotest
(newline)
(println :answer
(with-trace :add-5
(+ 2 3)))
(newline)
(println :using-eval
(eval
(quote (tst.demo.core/with-trace :the-ultimate-answer
(clojure.core/inc 41))))))
打印:
> lein clean ; lein test
*** running macro *** tag: :add-5
lein test _bootstrap
-------------------------------
Clojure 1.10.1 Java 14
-------------------------------
lein test tst.demo.core
:add-5 :enter
:add-5 :leave
:answer 5
*** running macro *** tag: :the-ultimate-answer
:the-ultimate-answer :enter
:the-ultimate-answer :leave
:using-eval 42
所以我们看到 eval
执行 "evaluate" 宏和函数。
更新
@amalloy 是正确的,我忘记引用我发送到 eval
的代码,因此宏 with-trace
是在编译时计算的,而不是 运行 时。在这个版本中,发送到 eval
的代码被引用,所以它只是一个包含 2 个符号、1 个关键字和一个整数的嵌套列表。现在,eval
看到宏,运行 生成实际代码,然后编译 & 运行 宏输出。