Ответ 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