Ну, вы уже разобрались с решением, так что несколько советов, чтобы объяснить поведение:
В Clojure (как в Lisp, Scheme и т. Д.) Все является выражением, а выражение является либо атомом, либо списком. Что касается списков, руководство Clojure гласит:
Непустые списки считаются звонками
в специальные формы, макросы или
функции. Звонок имеет форму
(операнды оператора *).
В вашем примере тело ((println x) (println x))
является списком, а оператор сам является выражением, которое Clojure должен вычислить для получения фактического оператора. То есть вы говорите «вычислите первое выражение и примите его возвращаемое значение как функцию, которая будет вызываться для второго выражения». Однако println
возвращает, как вы заметили, только nil
. Это приводит к NullPointerException
, если nil
интерпретируется как оператор.
Ваш код работает с (do (println x) (println x))
, потому что do
- это специальная форма, которая оценивает каждое выражение по очереди и возвращает значение последнего выражения. Здесь do
- оператор, а выражения с println
являются операндами.
Чтобы понять полезность этого поведения, обратите внимание, что функции являются первоклассными объектами в Clojure, например, вы можете вернуть функцию в результате другой функции. Например, возьмите следующий код:
(doseq [x '(1 2 3 4)] ((if (x > 2)
(fn [x] (println (+ x 2)))
(fn [x] (println (* x 3)))) x))
Здесь я динамически вычисляю оператор для вызова элемента в последовательности. Сначала оценивается if
-выражение. Если x
больше двух, if
преобразуется в функцию, которая печатает x + 2
, иначе он оценивается в функцию, которая печатает x * 3
. Эта функция применяется к x
последовательности.