为什么Clojure变量arnds args根据用途会有不同的类型?

时间:2014-09-25 15:50:29

标签: types clojure variadic-functions

在回答another question时,我发现了一些我没想到的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=> (def x (wtf 1 2 3 4))
clojure.lang.PersistentVector$ChunkedSeq
#'user/x
user=> x
:ok

为什么类型ArraySeq在1)和2)中,但是{3}中的PersistentVector$ChunkedSeq

2 个答案:

答案 0 :(得分:12)

简短回答:这是Clojure的一个模糊的实现细节。该语言唯一保证的是,如果没有其他参数,可变函数的rest-param将作为clojure.lang.ISeq的实例传递,或nil。你应该相应地编码。

长答案:它与函数调用是编译还是简单评估有关。如果不完整地讨论评估和编译之间的区别,那么知道Clojure代码被解析为AST就足够了。根据上下文,AST中的表达式可以直接进行评估(类似于解释),也可以作为动态生成的类的一部分编译成Java字节码。发生后者的典型情况是lambda表达式的主体,它将评估为实现IFn接口的动态生成的类的实例。有关评估的更详细说明,请参阅Clojure documentation

绝大多数情况下,编译代码和评估代码之间的差异对于您的程序是不可见的;他们的行为方式完全相同。这是罕见的极端情况之一,其中编译和评估导致微妙的不同行为。然而,重要的是要指出两种行为都是正确的,因为它们符合语言所作的承诺。

Clojure代码中的函数调用被解析为InvokeExprclojure.lang.Compiler的实例。如果正在编译代码,则编译器将使用适当的arity(Compiler.java, line 3650)发出将在invoke对象上调用IFn方法的字节码。如果代码只是被评估而不是编译,那么函数参数被捆绑在PersistentVector中并传递给applyTo对象(Compiler.java, line 3553)上的IFn方法

具有可变参数arg列表的Clojure函数被编译为clojure.lang.RestFn类的子类。此类实现IFn的所有方法,收集参数,并调度到适当的doInvoke arity。您可以在applyTo的实现中看到,在0需要args的情况下(如wtf函数中的情况),输入seq将传递给doInvoke方法并且对函数实现可见。同时,invoke的4-arg版本将ArraySeq中的参数捆绑在一起并将其传递给doInvoke方法,因此现在您的代码会看到ArraySeq。< / p>

为了使问题复杂化,Clojure的eval函数(这是REPL正在调用的函数)的实现将在内部包装一个在thunk(一个anoymous,no-arg函数)中被评估的列表表单,然后编译和执行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

答案 1 :(得分:1)

Clojure在很大程度上没有在类的方面实现,而是在接口和协议方面(对具有一些额外功能的Java接口的Clojure抽象)。

user> (require '[clojure.reflect :as reflect])
nil
user> (:bases (reflect/reflect clojure.lang.ArraySeq))
#{clojure.lang.IndexedSeq clojure.lang.IReduce clojure.lang.ASeq}
user> (:bases (reflect/reflect clojure.lang.PersistentVector$ChunkedSeq))
#{clojure.lang.Counted clojure.lang.IChunkedSeq clojure.lang.ASeq}

好的Clojure代码在ArraySeqPersistentVector$ChunkedSeq方面不起作用,而是调用IndexedSeqIReduce公开的方法或协议函数,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,你会发现基于所实现的接口或协议的各种实现:protocols.clj

(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