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
implementsDrawable
. 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.