Reloaded a protocol and "No implementation of method"?

While protocols provide a nice way to program to interfaces, they do have some unfortunate interactions with repl driven development. It's annoying to be working along, reload a file, and then BOOM.

1. Unhandled java.lang.IllegalArgumentException
   No implementation of method: :draw of protocol: #'z.draw/Drawable
   found for class: z.core.Rectangle

But this code was just working! My Rectangle implements Drawable. It's right there. I can see it in the code!

Having code that looks like it should work, but getting runtime errors is annoying. Getting ones that aren't immediately understandable, about code that was just working, can frustrate anyone.

So what causes this error?

To duplicate this error imagine working on two files.

(ns z.core
  (:require [z.draw :as d]
            [clojure.string :as str]))

(defrecord Rectangle [width height]
  d/Drawable
  (draw [t] (str "I'm a " width "x" height " rectangle!")))

(defn draw-many-rectangles []
  (let [rs (map ->Rectangle (range 3) (range 3))]
    (d/draw-many rs)))
(ns z.draw)

(defprotocol Drawable
  (draw [t]))

(defn draw-many [shapes]
  (map draw shapes))

Loading these files into a repl works. Calling draw-many-rectangles outputs something expected.

z.core> (draw-many-rectangles)
("I'm a 0x0 rectangle!" "I'm a 1x1 rectangle!" "I'm a 2x2 rectangle!")

Now imagine having to change what the output is. Perhaps instead of a list of strings draw-many needs to return a single string.

(ns z.draw
  (:require [clojure.string :as str]))

(defprotocol Drawable
  (draw [t]))

(defn draw-many [shapes]
  (str/join "\n" (map draw shapes)))

A common workflow is to reload a file into a repl once editing it is done. This could be with cider's C-c C-x, or clojure's load and load-file functions. Doing this for the z.draw namespace and attempting to run (draw-many-rectangles) will cause an error.

1. Unhandled java.lang.IllegalArgumentException
   No implementation of method: :draw of protocol: #'z.draw/Drawable
   found for class: z.core.Rectangle

              core_deftype.clj:  554  clojure.core/-cache-protocol-fn
                      draw.clj:    4  z.draw/eval8029/fn/G
                      core.clj: 2622  clojure.core/map/fn
                  LazySeq.java:   40  clojure.lang.LazySeq/sval
                  LazySeq.java:   49  clojure.lang.LazySeq/seq
                  LazySeq.java:   71  clojure.lang.LazySeq/first
                       RT.java:  653  clojure.lang.RT/first
                      core.clj:   55  clojure.core/first
                    string.clj:  185  clojure.string/join
                      draw.clj:    8  z.draw/draw-many
                      core.clj:   11  z.core/draw-many-rectangles
                          REPL:    1  z.core/eval8047
...

This happens because when the z.draw namespace was reloaded it created a new protocol with the same name. But the z.core namespace was evaluated with the previous reference. It's defrecord referred to the old version of Drawable. When creating a Rectangle it will implement the old protocol.

How to fix

The z.core namespace needs to be reloaded. Then it will redefine Rectangle and refer to the correct Drawable. This can be done by using the same load-file technique that loaded z.draw.

Doing this by hand can be annoying. There are techniques to reduce the possibility of encountering this. One is to only define a protocol in a other wise empty namespace. All other functions go elsewhere. Then these types of errors can only occur when changing the protocol definition.

A tooling based solution is to use tools.namespace and something similar to reloadable systems in order to reload your entire application at once. I've been told cursive's "Load File in REPL" command implements a similar namespace reloading mechanism.