Types 为什么Clojure变量arity参数会根据使用情况获得不同的类型?
在回答时,我遇到了Clojure的变量arity函数args中我没有预料到的东西:Types 为什么Clojure变量arity参数会根据使用情况获得不同的类型?,types,clojure,variadic-functions,Types,Clojure,Variadic Functions,在回答时,我遇到了Clojure的变量arity函数args中我没有预料到的东西: user=> (defn wtf [& more] (println (type more)) :ok) #'user/wtf ;; 1) user=> (wtf 1 2 3 4) clojure.lang.ArraySeq :ok ;; 2) user=> (let [x (wtf 1 2 3 4)] x) clojure.lang.ArraySeq :ok ;; 3) user
user=> (defn wtf [& more] (println (type more)) :ok)
#'user/wtf
;; 1)
user=> (wtf 1 2 3 4)
clojure.lang.ArraySeq
:ok
;; 2)
user=> (let [x (wtf 1 2 3 4)] x)
clojure.lang.ArraySeq
:ok
;; 3)
user=> (def x (wtf 1 2 3 4))
clojure.lang.PersistentVector$ChunkedSeq
#'user/x
user=> x
:ok
为什么1)和2)中的类型
ArraySeq
,而3)中的类型PersistentVector$ChunkedSeq
。Clojure在很大程度上不是根据类实现的,而是根据接口和协议实现的(java接口上的Clojure抽象,具有一些额外的特性)
好的Clojure代码在ArraySeq
与PersistentVector$ChunkedSeq
的比较中不起作用,而是调用IndexedSeq
、IReduce
、ASeq
等公开的方法或协议函数,如果它们的参数实现了它们。或者更可能使用基本的clojure.core
函数,这些函数是根据这些接口或协议实现的
例如,请注意reduce
的定义:
user> (source reduce)
(defn reduce
"f should be a function of 2 arguments. If val is not supplied,
returns the result of applying f to the first 2 items in coll, then
applying f to that result and the 3rd item, etc. If coll contains no
items, f must accept no arguments as well, and reduce returns the
result of calling f with no arguments. If coll has only 1 item, it
is returned and f is not called. If val is supplied, returns the
result of applying f to val and the first item in coll, then
applying f to that result and the 2nd item, etc. If coll contains no
items, returns val and f is not called."
{:added "1.0"}
([f coll]
(clojure.core.protocols/coll-reduce coll f))
([f val coll]
(clojure.core.protocols/coll-reduce coll f val)))
nil
如果查找coll reduce
,您会发现基于实现的接口或协议的各种实现:
简短回答:这是Clojure的一个模糊实现细节。该语言唯一能保证的是变量函数的rest参数将作为
clojure.lang.ISeq
的实例传递,如果没有其他参数,则作为nil
的实例传递。您应该相应地编写代码
长答案:它与函数调用是编译还是简单计算有关。不必对计算和编译之间的区别进行完整的论述,只需知道Clojure代码被解析为AST就足够了。根据上下文的不同,AST中的表达式可以直接进行计算(类似于解释),也可以作为动态生成的类的一部分编译成Java字节码。后一种情况发生在lambda表达式的主体中,它将计算为实现
IFn
接口的动态生成类的实例。有关评估的更详细说明,请参见
在大多数情况下,编译代码和计算代码之间的差异对程序来说是看不见的;他们将以完全相同的方式行事。这是编译和评估导致细微不同行为的极少数情况之一。但是,需要指出的是,这两种行为都是正确的,因为它们都符合语言的承诺
Clojure代码中的函数调用被解析为Clojure.lang.Compiler
中的InvokeExpr
实例。如果正在编译代码,则编译器将发出字节码,该字节码将使用适当的arity()调用IFn
对象上的invoke
方法。如果只是对代码求值而没有编译,那么函数参数将捆绑在一个PersistentVector
中,并传递给IFn
对象()上的applyTo
方法
具有可变参数列表的Clojure函数被编译成类的子类。此类实现了IFn的所有方法,收集参数,并将其分派到相应的doInvoke
arity。您可以在applyTo
的实现中看到,在0个必需参数的情况下(与wtf
函数的情况一样),输入序列被传递到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
它变得更好:
((fn[](def x(wtf 1 2 3 4)))
我同意这里所说的,但它没有解决问题。我可能没有足够明确地将它与问题联系起来,但这是我尝试的“为什么”(而@Alex解决的是“如何”)。我的问题可能更为尖锐。尽管如此,我还是很感谢你抽出时间来回答。谢谢你。你能澄清一下“显式地计算代码而不编译”是什么意思吗?计算未编译的代码意味着什么?Clojure代码被解析为AST。根据上下文的不同,AST中的表达式可以直接进行计算(类似于解释),也可以作为动态生成的类的一部分编译成Java字节码。发生这种情况的典型案例是lambda表达式的主体,它将计算为实现IFn
接口的动态生成类的实例。我知道这偏离了主题,但。。。没有编译成字节码的东西:你说的是像符号解析这样的东西,以及像关键字、字符串、数字等这样的对自己进行计算的形式吗?是的,事实证明,由于计算将大多数列表形式封装在匿名fn
中,所以你提到的那些情况,以及def
形式的特例,是唯一可以避免字节码生成的实例。如果您能提供
(extend-protocol CollReduce
nil
(coll-reduce
([coll f] (f))
([coll f val] val))
Object
(coll-reduce
([coll f] (seq-reduce coll f))
([coll f val] (seq-reduce coll f val)))
clojure.lang.IReduce
(coll-reduce
([coll f] (.reduce coll f))
([coll f val] (.reduce coll f val)))
;;aseqs are iterable, masking internal-reducers
clojure.lang.ASeq
(coll-reduce
([coll f] (seq-reduce coll f))
([coll f val] (seq-reduce coll f val)))
...) ; etcetera
user=> (apply wtf [1 2 3 4])
clojure.lang.PersistentVector$ChunkedSeq
:ok
user=> (apply wtf (list 1 2 3 4))
clojure.lang.PersistentList
:ok