简短回答:这是 Clojure 的一个晦涩的实现细节。该语言唯一保证的是可变参数函数的剩余参数将作为clojure.lang.ISeq
, or nil
如果没有其他参数。您应该相应地编码。
长答案:它与函数调用是编译还是简单评估有关。无需深入探讨求值和编译之间的差异,了解 Clojure 代码被解析为 AST 就足够了。根据上下文,AST 中的表达式可以直接求值(类似于解释),也可以编译为 Java 字节码,作为动态生成的类的一部分。后者发生的典型情况是在 lambda 表达式的主体中,该表达式将计算为动态生成的类的实例,该类实现了IFn
界面。请参阅Clojure 文档 http://clojure.org/evaluation以获得更详细的评估解释。
绝大多数时候,编译代码和评估代码之间的差异对于您的程序来说是不可见的;他们的行为方式完全相同。这是罕见的极端情况之一,编译和评估会导致行为略有不同。不过,需要指出的是,这两种行为都是正确的,因为它们符合语言做出的承诺。
Clojure 代码中的函数调用被解析为一个实例InvokeExpr
in clojure.lang.Compiler
。如果正在编译代码,则编译器会发出字节码,该字节码将调用invoke
上的方法IFn
使用适当的数量的对象(Compiler.java,第 3650 行 https://github.com/clojure/clojure/blob/clojure-1.6.0/src/jvm/clojure/lang/Compiler.java#L3650)。如果代码只是被评估而不是被编译,那么函数参数将被捆绑在一个PersistentVector
并传递给applyTo
方法上的IFn
目的 (Compiler.java,第 3553 行 https://github.com/clojure/clojure/blob/clojure-1.6.0/src/jvm/clojure/lang/Compiler.java#L3553).
具有可变参数列表的 Clojure 函数被编译成clojure.lang.RestFn https://github.com/clojure/clojure/blob/clojure-1.6.0/src/jvm/clojure/lang/RestFn.java班级。这个类实现了所有的方法IFn
,收集参数,并分派到适当的doInvoke
数量。你可以在执行中看到applyTo
也就是说,在 0 个必需参数的情况下(就像你的情况一样)wtf
函数),输入 seq 被传递到doInvoke
方法并对函数实现可见。 4-arg 版本invoke
同时,将参数捆绑在一个ArraySeq
并将其传递给doInvoke
方法,所以现在你的代码看到一个ArraySeq
.
让事情变得复杂的是,Clojure 的实现eval
函数(这是 REPL 所调用的)将在内部包装一个在 thunk(一个匿名的无参数函数)内评估的列表形式,然后编译并执行该 thunk。因此几乎所有调用都使用对invoke
方法,而不是由编译器直接解释。有一个特殊情况def
显式评估代码而不进行编译的表单,这解释了您在那里看到的不同行为。
实施clojure.core/apply
也称为applyTo
方法,并通过此逻辑传递给任何列表类型apply
应该看到函数体。的确:
user=> (apply wtf [1 2 3 4])
clojure.lang.PersistentVector$ChunkedSeq
:ok
user=> (apply wtf (list 1 2 3 4))
clojure.lang.PersistentList
:ok