ClojureScript method head recur in defrecord

June 27, 2017

An update in the ClojureScript 1.9.655 compiler release eliminates a difference with respect to Clojure for code making a recur to a defrecord method head. Hopefully this post is useful in the event that you encounter this situation!




Let's look at some example code to make things concrete.

Let's say you have a protocol:

(defprotocol ISearch
  (search [this coll]))

And, let's say you are making an instance using defrecord. Notice that the recur is to the search method head (and not a loop target):

(defrecord Search [item]
  ISearch
  (search [this coll]
   (when (seq coll)
    (if (= item (first coll))
     item
     (recur (rest coll))))))

The above is a contrived example. It will scan for and return an item if it is found in a collection. For example, this

(-> (->Search 1)
  (search [:a 1 "b"]))

yields 1, while

(-> (->Search :z) 
  (search [:a 1 "b"]))

yields nil.

The code above works fine in Clojure, but if you try it in ClojureScript 1.9.562 or earlier, you will get the following when attempting to evaluate the defrecord form:

recur argument count mismatch at line 7
…

“Ahh, of course,” I'm thinking, “I need to add this to the recur call.” And I revise it so that part looks like:

     (recur this (rest coll))

Things are working now. ¯\_(ツ)_/¯

But, this is actually not the right thing to do—the docstring for defrecord has this to say:

… Note also that recur calls to the method
head should *not* pass the target object,
it will be supplied automatically and can 
not be substituted.

If you are trying to write portable code (say, using .cljc), then you'd need to deal with this discrepancy.

This has been fixed with CLJS-2085, making it so that you can now compile the code as initially written above. Note that this applies to defrecord, deftype, and reify.

In order to not break existing ClojureScript code that is passing this (or some other value) as the first argument to recur, the compiler will continue to accept the code, but it will emit a diagnostic like this:

WARNING: Ignoring target object "this" passed in recur to protocol method head at line 7

So, if you happen to see this warning, and you know your code will only be used in ClojureScript 1.9.655 or later, you can safely remove the first argument to recur.

If you are seeing this warning being emitted for library code that cannot be updated, you can suppress :protocol-impl-recur-with-target. See Compiler Warnings.

If you are a library maintainer and wish to continue supporting ClojureScript 1.9.562 and earlier, prior code will continue to work, albeit while emitting a warning. If you'd like to avoid this warning, one optional workaround to consider is adding an explicit top-level loop target in your method implementation.

Tags: ClojureScript