Почему Clojure переменные arity args получают разные типы в зависимости от использования?

Отвечая на еще один вопрос, я наткнулся на то, чего не ожидал с Clojure переменной arity function 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), но PersistentVector$ChunkedSeq в 3)?

Ответы

Ответ 1

Краткий ответ: Это неясная деталь реализации Clojure. Единственное, что гарантируется языком, это то, что параметр rest для вариационной функции будет передан как экземпляр clojure.lang.ISeq или nil, если дополнительных аргументов нет. Вы должны соответствующим образом закодировать.

Длинный ответ: Он связан с тем, компилируется или просто вычисляется вызов функции. Не вдаваясь в полную диссертацию о различии между оценкой и компиляцией, должно быть достаточно знать, что код Clojure анализируется в АСТ. В зависимости от контекста выражения в AST могут оцениваться напрямую (что-то вроде интерпретации) или могут быть скомпилированы в байт-код Java как часть динамически генерируемого класса. Типичный случай, когда последнее происходит, находится в теле лямбда-выражения, которое будет оцениваться экземпляром динамически сгенерированного класса, реализующего интерфейс IFn. Более подробное объяснение оценки можно найти в Clojure документации.

В большинстве случаев разница между скомпилированным и оцененным кодом будет невидимой для вашей программы; они будут вести себя точно так же. Это один из тех редких угловых случаев, когда компиляция и оценка приводят к малому поведению. Важно отметить, однако, что оба поведения верны в том, что они соответствуют promises, сделанным языком.

Вызов функций в Clojure код получает разбор в экземпляр InvokeExpr в clojure.lang.Compiler. Если код компилируется, компилятор испускает байт-код, который будет вызывать метод invoke для объекта IFn, используя соответствующую arity (Compiler.java, строка 3650). Если код просто оценивается и не компилируется, то аргументы функции объединяются в PersistentVector и передаются методу applyTo объекта IFn (Compiler.java, строка 3553).

Clojure функции, имеющие переменный список аргументов, скомпилированы в подклассы класса clojure.lang.RestFn. Этот класс реализует все методы IFn, собирает аргументы и отправляет в соответствующую doInvoke arity. Вы можете увидеть в реализации applyTo, что в случае 0 обязательных аргументов (как в случае с вашей функцией wtf) вход seq передается методу doInvoke и видим реализации функции, Тем не менее, версия 4-arg invoke связывает аргументы в ArraySeq и передает это методу doInvoke, поэтому теперь ваш код видит ArraySeq.

Чтобы усложнить задачу, реализация функции Clojure eval (которая является вызовом REPL) будет внутренне обертывать форму списка, которая оценивается внутри thunk (anoymous, no-arg function), а затем компилировать и выполнить 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

Ответ 2

Clojure по большей части не реализован в терминах классов, а в терминах интерфейсов и протоколов (абстракция Clojure через java-интерфейсы с несколькими дополнительными функциями).

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}

good Clojure код не работает в терминах ArraySeq vs. 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, вы найдете различные реализации на основе реализованных интерфейсов или протоколов: 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