Share your thoughts in the 2024 State of Clojure Survey!

Welcome! Please see the About page for a little more info on how this works.

0 votes
in Spec by
edited by
$ clj
Clojure 1.10.1
user=> (-> :hmm (fn [x] x))
Syntax error macroexpanding clojure.core/fn at (REPL:1:1).
:hmm - failed: vector? at: [:fn-tail :arity-1 :params] spec: :clojure.core.specs.alpha/param-list
:hmm - failed: (or (nil? %) (sequential? %)) at: [:fn-tail :arity-n] spec: :clojure.core.specs.alpha/params+body


$ clj --main cljs.main --repl
cljs.user=>  (-> :hmm (fn [x] x))
Unexpected error (IllegalArgumentException) macroexpanding cljs.core/fn at (<cljs repl>:1:11).
Parameter declaration :hmm should be a vector

A workaround is to wrap the lambda in another parens like ((fn [x] x))

I tried to run clojure with the latest spec.alpha from git, but it didn't start. So I'm not sure how to attempt to debug this.

deps.edn:

{:deps {org.clojure/clojure       {:mvn/version "1.10.1"
                                   :exclusions  [org.clojure/spec.alpha]}
        org.clojure/clojurescript {:git/url "https://github.com/clojure/clojurescript"
                                   :sha     "dcc8e61c79bfc701fe9e1414fe5db93edf6f1853"}
        org.clojure/spec.alpha    {:git/url "https://github.com/clojure/spec.alpha"
                                   :sha     "cd4aeb7edccbf7a881dcdc3c947313359508db3a"}}}

repl:

$ clj
Exception in thread "main" java.lang.ExceptionInInitializerError
	at java.base/java.lang.Class.forName0(Native Method)
	at java.base/java.lang.Class.forName(Class.java:398)
	at clojure.lang.RT.classForName(RT.java:2211)
	at clojure.lang.RT.classForName(RT.java:2220)
	at clojure.lang.RT.loadClassForName(RT.java:2239)
	at clojure.lang.RT.load(RT.java:449)
	at clojure.lang.RT.load(RT.java:424)
	at clojure.core$load$fn__6839.invoke(core.clj:6126)
	at clojure.core$load.invokeStatic(core.clj:6125)
	at clojure.core$load.doInvoke(core.clj:6109)
	at clojure.lang.RestFn.invoke(RestFn.java:408)
	at clojure.core$load_one.invokeStatic(core.clj:5908)
	at clojure.core$load_one.invoke(core.clj:5903)
	at clojure.core$load_lib$fn__6780.invoke(core.clj:5948)
	at clojure.core$load_lib.invokeStatic(core.clj:5947)
	at clojure.core$load_lib.doInvoke(core.clj:5928)
	at clojure.lang.RestFn.applyTo(RestFn.java:142)
	at clojure.core$apply.invokeStatic(core.clj:667)
	at clojure.core$load_libs.invokeStatic(core.clj:5985)
	at clojure.core$load_libs.doInvoke(core.clj:5969)
	at clojure.lang.RestFn.applyTo(RestFn.java:137)
	at clojure.core$apply.invokeStatic(core.clj:667)
	at clojure.core$require.invokeStatic(core.clj:6007)
	at clojure.core.server$loading__6721__auto____8842.invoke(server.clj:9)
	at clojure.core.server__init.load(Unknown Source)
	at clojure.core.server__init.<clinit>(Unknown Source)
	at java.base/java.lang.Class.forName0(Native Method)
	at java.base/java.lang.Class.forName(Class.java:398)
	at clojure.lang.RT.classForName(RT.java:2211)
	at clojure.lang.RT.classForName(RT.java:2220)
	at clojure.lang.RT.loadClassForName(RT.java:2239)
	at clojure.lang.RT.load(RT.java:449)
	at clojure.lang.RT.load(RT.java:424)
	at clojure.core$load$fn__6839.invoke(core.clj:6126)
	at clojure.core$load.invokeStatic(core.clj:6125)
	at clojure.core$load.doInvoke(core.clj:6109)
	at clojure.lang.RestFn.invoke(RestFn.java:408)
	at clojure.core$load_one.invokeStatic(core.clj:5908)
	at clojure.core$load_one.invoke(core.clj:5903)
	at clojure.core$load_lib$fn__6780.invoke(core.clj:5948)
	at clojure.core$load_lib.invokeStatic(core.clj:5947)
	at clojure.core$load_lib.doInvoke(core.clj:5928)
	at clojure.lang.RestFn.applyTo(RestFn.java:142)
	at clojure.core$apply.invokeStatic(core.clj:667)
	at clojure.core$load_libs.invokeStatic(core.clj:5985)
	at clojure.core$load_libs.doInvoke(core.clj:5969)
	at clojure.lang.RestFn.applyTo(RestFn.java:137)
	at clojure.core$apply.invokeStatic(core.clj:667)
	at clojure.core$require.invokeStatic(core.clj:6007)
	at clojure.core$require.doInvoke(core.clj:6007)
	at clojure.lang.RestFn.invoke(RestFn.java:408)
	at clojure.lang.Var.invoke(Var.java:384)
	at clojure.lang.RT.doInit(RT.java:491)
	at clojure.lang.RT.init(RT.java:467)
	at clojure.main.main(main.java:38)
Caused by: Syntax error macroexpanding clojure.core/defn at (clojure/spec/alpha.clj:85:1).
	at clojure.lang.Compiler.checkSpecs(Compiler.java:6972)
	at clojure.lang.Compiler.macroexpand1(Compiler.java:6988)
	at clojure.lang.Compiler.macroexpand(Compiler.java:7075)
	at clojure.lang.Compiler.eval(Compiler.java:7161)
	at clojure.lang.Compiler.load(Compiler.java:7636)
	at clojure.lang.RT.loadResourceScript(RT.java:381)
	at clojure.lang.RT.loadResourceScript(RT.java:372)
	at clojure.lang.RT.load(RT.java:459)
	at clojure.lang.RT.load(RT.java:424)
	at clojure.core$load$fn__6839.invoke(core.clj:6126)
	at clojure.core$load.invokeStatic(core.clj:6125)
	at clojure.core$load.doInvoke(core.clj:6109)
	at clojure.lang.RestFn.invoke(RestFn.java:408)
	at clojure.core$load_one.invokeStatic(core.clj:5908)
	at clojure.core$load_one.invoke(core.clj:5903)
	at clojure.core$load_lib$fn__6780.invoke(core.clj:5948)
	at clojure.core$load_lib.invokeStatic(core.clj:5947)
	at clojure.core$load_lib.doInvoke(core.clj:5928)
	at clojure.lang.RestFn.applyTo(RestFn.java:142)
	at clojure.core$apply.invokeStatic(core.clj:667)
	at clojure.core$load_libs.invokeStatic(core.clj:5985)
	at clojure.core$load_libs.doInvoke(core.clj:5969)
	at clojure.lang.RestFn.applyTo(RestFn.java:137)
	at clojure.core$apply.invokeStatic(core.clj:667)
	at clojure.core$require.invokeStatic(core.clj:6007)
	at clojure.main$loading__6721__auto____8974.invoke(main.clj:11)
	at clojure.main__init.load(Unknown Source)
	at clojure.main__init.<clinit>(Unknown Source)
	... 55 more
Caused by: java.lang.Exception: #object[clojure.spec.alpha$and_spec_impl$reify__1049 0x1292071f "clojure.spec.alpha$and_spec_impl$reify__1049@1292071f"] is not a fn, expected predicate fn
	at clojure.spec.alpha$dt.invokeStatic(alpha.clj:769)
	at clojure.spec.alpha$dt.invoke(alpha.clj:759)
	at clojure.spec.alpha$dt.invokeStatic(alpha.clj:760)
	at clojure.spec.alpha$dt.invoke(alpha.clj:759)
	at clojure.spec.alpha$deriv.invokeStatic(alpha.clj:1534)
	at clojure.spec.alpha$deriv.invoke(alpha.clj:1528)
	at clojure.spec.alpha$deriv.invokeStatic(alpha.clj:1542)
	at clojure.spec.alpha$deriv.invoke(alpha.clj:1528)
	at clojure.spec.alpha$deriv$fn__1291.invoke(alpha.clj:1544)
	at clojure.core$map$fn__5866.invoke(core.clj:2755)
	at clojure.lang.LazySeq.sval(LazySeq.java:42)
	at clojure.lang.LazySeq.seq(LazySeq.java:51)
	at clojure.lang.RT.seq(RT.java:535)
	at clojure.core$seq__5402.invokeStatic(core.clj:137)
	at clojure.core$map$fn__5873.invoke(core.clj:2763)
	at clojure.lang.LazySeq.sval(LazySeq.java:42)
	at clojure.lang.LazySeq.seq(LazySeq.java:51)
	at clojure.lang.RT.seq(RT.java:535)
	at clojure.core$seq__5402.invokeStatic(core.clj:137)
	at clojure.core$filter$fn__5893.invoke(core.clj:2809)
	at clojure.lang.LazySeq.sval(LazySeq.java:42)
	at clojure.lang.LazySeq.seq(LazySeq.java:51)
	at clojure.lang.RT.seq(RT.java:535)
	at clojure.core$seq__5402.invokeStatic(core.clj:137)
	at clojure.core$map$fn__5866.invoke(core.clj:2746)
	at clojure.lang.LazySeq.sval(LazySeq.java:42)
	at clojure.lang.LazySeq.seq(LazySeq.java:51)
	at clojure.lang.RT.seq(RT.java:535)
	at clojure.core$seq__5402.invokeStatic(core.clj:137)
	at clojure.core$seq__5402.invoke(core.clj:137)
	at clojure.spec.alpha$filter_alt.invokeStatic(alpha.clj:1431)
	at clojure.spec.alpha$filter_alt.invoke(alpha.clj:1425)
	at clojure.spec.alpha$alt_STAR_.invokeStatic(alpha.clj:1435)
	at clojure.spec.alpha$alt_STAR_.invoke(alpha.clj:1434)
	at clojure.spec.alpha$deriv.invokeStatic(alpha.clj:1544)
	at clojure.spec.alpha$deriv.invoke(alpha.clj:1528)
	at clojure.spec.alpha$deriv.invokeStatic(alpha.clj:1542)
	at clojure.spec.alpha$deriv.invoke(alpha.clj:1528)
	at clojure.spec.alpha$deriv.invokeStatic(alpha.clj:1543)
	at clojure.spec.alpha$deriv.invoke(alpha.clj:1528)
	at clojure.spec.alpha$deriv.invokeStatic(alpha.clj:1543)
	at clojure.spec.alpha$deriv.invoke(alpha.clj:1528)
	at clojure.spec.alpha$re_conform.invokeStatic(alpha.clj:1669)
	at clojure.spec.alpha$re_conform.invoke(alpha.clj:1660)
	at clojure.spec.alpha$regex_spec_impl$reify__1375.conform_STAR_(alpha.clj:1710)
	at clojure.spec.alpha$conform.invokeStatic(alpha.clj:171)
	at clojure.spec.alpha$conform.invoke(alpha.clj:167)
	at clojure.spec.alpha$macroexpand_check.invokeStatic(alpha.clj:708)
	at clojure.spec.alpha$macroexpand_check.invoke(alpha.clj:704)
	at clojure.lang.AFn.applyToHelper(AFn.java:156)
	at clojure.lang.AFn.applyTo(AFn.java:144)
	at clojure.lang.Var.applyTo(Var.java:705)
	at clojure.lang.Compiler.checkSpecs(Compiler.java:6970)
	... 82 more

2 Answers

0 votes
by
selected by
 
Best answer

The error you are getting is not due to a bug but rather due to the way that the thread macro is expanding your expression. One way to debug this issue is to use macroexpand-1 at the REPL to see what the resulting code would be.

user=> (macroexpand-1 '(-> :hmm (fn [x] x)))
(fn :hmm [x] x)

The thread-first macro will take the first item and shove it into the first position of the following next expression and repeat until the end of the expression. In this case, the expression expands into an invalid fn form. (Try using hmm instead of :hmm for fun!)

This also explains why your workaround works. By wrapping the anonymous fn in an extra set of parens, you are making the expression to call the fn and then threading in the argument to the call:

user=> (macroexpand-1 '(-> :hmm ((fn [x] x))))
((fn [x] x) :hmm)

In my experience, you do not frequently run into this issue because you usually reference other functions by name when threading, and the -> macro handles this case explicitly by converting into a list form if necessary:

user=> (macroexpand-1 '(-> :hmm my-fn))
(my-fn :hmm)
by
edited by
Thank you for the detailed answer!  I totally get it now.  I forgot that it's a macro and it treats everything completely literally just as data.  However, as you said, it has a special case for a bare symbol.  My sense of how the thread macro "should" work is to handle the (fn [x] ,,,) form the same way it handles a symbol, by wrapping it in a list first...
0 votes
by

The thread macros are syntactic manipulations - they don’t evalutate the fn and invoke it.

The macroexpanded code looks like:

(fn :hmm [x] x)

As the spec is telling you, this is invalid. So this is all working as designed.

by
Yes that makes perfect sense.  It's obviously not a bug.  But it does feel intuitively "right" to do what I did, and I see my use to be equivalent to passing an already symbolized fn without parens.  Not literally, but on the level of intention.  Would it make sense to add a special case for the (fn [x] ,,,) form to wrap it in a list and give it a single arg?
by
edited by
How about this, @alexmiller?  What are the chances of getting this change into core?  ;)

From c18ca3c055b8267fed84e40cb3dcd9513c263599 Mon Sep 17 00:00:00 2001
From: Thomas Spellman <[email protected]>
Date: Sun, 12 Jan 2020 11:07:42 -0800
Subject: [PATCH] thread lambdas

---
 src/clj/clojure/core.clj             | 14 +++++++++-----
 test/clojure/test_clojure/macros.clj | 12 ++++++++++++
 2 files changed, 21 insertions(+), 5 deletions(-)

diff --git a/src/clj/clojure/core.clj b/src/clj/clojure/core.clj
index 8e98e072..27d22558 100644
--- a/src/clj/clojure/core.clj
+++ b/src/clj/clojure/core.clj
@@ -1671,43 +1671,47 @@
 
   but is easier to write, read, and understand."
   {:added "1.0"}
   ([x form] `(. ~x ~form))
   ([x form & more] `(.. (. ~x ~form) ~@more)))
 
 (defmacro ->
   "Threads the expr through the forms. Inserts x as the
-  second item in the first form, making a list of it if it is not a
+  second item in the first form, making a list of it if it is a lambda or not a
   list already. If there are more forms, inserts the first form as the
   second item in second form, etc."
   {:added "1.0"}
   [x & forms]
   (loop [x x, forms forms]
     (if forms
       (let [form (first forms)
             threaded (if (seq? form)
-                       (with-meta `(~(first form) ~x ~@(next form)) (meta form))
+                       (if (#{'fn 'fn*} (first form))
+                         (list form x)
+                         (with-meta `(~(first form) ~x ~@(next form)) (meta form)))
                        (list form x))]
         (recur threaded (next forms)))
       x)))
 
 (defmacro ->>
   "Threads the expr through the forms. Inserts x as the
-  last item in the first form, making a list of it if it is not a
+  last item in the first form, making a list of it if it is a lambda or not a
   list already. If there are more forms, inserts the first form as the
   last item in second form, etc."
   {:added "1.1"}
   [x & forms]
   (loop [x x, forms forms]
     (if forms
       (let [form (first forms)
             threaded (if (seq? form)
-              (with-meta `(~(first form) ~@(next form)  ~x) (meta form))
-              (list form x))]
+                       (if (#{'fn 'fn*} (first form))
+                         (list form x)
+                         (with-meta `(~(first form) ~@(next form)  ~x) (meta form)))
+                       (list form x))]
         (recur threaded (next forms)))
       x)))
 
 (def map)
 
 (defn ^:private check-valid-options
   "Throws an exception if the given option map contains keys not listed
   as valid, else returns nil."
diff --git a/test/clojure/test_clojure/macros.clj b/test/clojure/test_clojure/macros.clj
index ce17bb38..5ebd2dd5 100644
--- a/test/clojure/test_clojure/macros.clj
+++ b/test/clojure/test_clojure/macros.clj
@@ -106,8 +106,20 @@
   (is (nil? (loop []
               (as-> 0 x
                 (when-not (zero? x)
                   (recur))))))
   (is (nil? (loop [x nil] (some-> x recur))))
   (is (nil? (loop [x nil] (some->> x recur))))
   (is (= 0 (loop [x 0] (cond-> x false recur))))
   (is (= 0 (loop [x 0] (cond->> x false recur)))))
+
+(deftest ->lambda-test
+  (is (= 'a (-> 'a ((fn [x] x)))))
+  (is (= 'a (-> 'a (fn [x] x))))
+  (is (= 'a (-> 'a #(identity %))))
+  (is (= 'a (-> 'a (#(identity %))))))
+
+(deftest ->>lambda-test
+  (is (= 'a (->> 'a ((fn [x] x)))))
+  (is (= 'a (->> 'a (fn [x] x))))
+  (is (= 'a (->> 'a #(identity %))))
+  (is (= 'a (->> 'a (#(identity %))))))
\ No newline at end of file
--
2.21.0 (Apple Git-122.2)

it works!

(-> :hmm (fn [x] x))
=> :hmm

and tests pass!

test:
[INFO] Executed tasks
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  01:18 min
[INFO] Finished at: 2020-01-12T10:58:31-08:00
[INFO] ------------------------------------------------------------------------
...