Clojure如何编译在REPL中运行的代码?

3
诚实的新手问题。基于Russ Olsen的《学习Clojure》(Getting Clojure),我知道以下内容:
(1) 在运行时,Clojure代码会被编译为JVM字节码。
(2) 使用REPL可以运行Clojure代码,并获得几乎即时的反馈,REPL使用函数(read)和(eval)或等价函数。
因此,在REPL中,Clojure代码编译成JVM字节码必须在某个时刻发生,可能是在(read)阶段或稍后的某个阶段。
但这是一个模糊的想象,我想要澄清一下。
例如,知道在REPL何时编译代码,编译产生的数据如何存储在内存中,然后由(eval)访问,以及之间或之后发生的任何重要步骤,都是很好的了解。
换句话说,我想要更详细地了解制作香肠的过程:
Clojure如何编译在REPL中运行的代码?
(加分题:与Clojure从非REPL源(例如Leiningen项目)编译代码有何不同?)
2个回答

5

读取器(reader)会消耗字符并生成Clojure数据结构(列表、向量、符号等)。读取阶段肯定不知道任何有关JVM字节码的信息。这是作为评估阶段的一部分发生的:编译器会消耗这些数据结构并生成JVM字节码。

在运行REPL时,该字节码存储在DynamicClassLoader中-所有JVM类都必须由某个ClassLoader定义,而DynamicClassLoader是Clojure创建的允许从Clojure数据结构动态定义类的ClassLoader之一。

当编译为类文件时,同样的字节码只需写入磁盘上的.class文件中,然后可能打包到一个jar文件中。


由于编译严格发生在(eval)期间,我是否可以认为宏在(read)之后但(eval)之前被展开? - Asker
1
宏展开是编译器执行的一步。如果它想要评估一个表单,并且检测到该表单表示使用宏,那么在继续之前会先扩展宏。 - amalloy

1
一项简短的测试揭示了答案:

(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不仅可以“评估”函数,还可以评估宏。

更新

@amalloy是正确的,我忘记引用发送到eval中的代码,因此宏with-trace在编译时而不是运行时被评估。使用此版本,发送到eval中的代码已被引用,因此它只是一个嵌套的列表,包含2个符号、1个关键字和一个整数。现在,eval看到了宏,运行它以生成实际的代码,然后编译并运行宏输出。


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