Kris
Kris

Reputation: 19948

How to reload a multimethod in Clojure REPL

I am writing a multimethod in the REPL, functions can be redefined just fine, but if I redefine the dispatch function of a multi method it seems not to use the newly refined function:

;; simple fn to resolve defmethod to call, hardcoded to :do-it
(defn resolve-it [] :do-it)
(resolve-it) ;; :do-it, as expected

(defmulti do-something resolve-it)

(defmethod do-something :do-it [] (println "do-it"))
(defmethod do-something :oh-no [] (println "oh-no"))

(do-something) ;; "do-it", as expected

;; now change resolve-it
(defn resolve-it [] :oh-no)
(resolve-it) ;; :oh-no, as expected

(do-something) ;; "do-it", not as expected

(do-something) ;; "do-it", not expected

How do I get the mult imethod to reflect changes to the dispatch function, resolve-it?

Upvotes: 11

Views: 2995

Answers (4)

Didier A.
Didier A.

Reputation: 4816

Another little trick is to def the var to nil first:

(defmulti hello #(do (println "Hello") %))
(defmethod hello :world ([_] (println "World!")))

;> (hello :world)
Hello
World!

(def hello nil) ;; Eval this and you can now redef the defmulti
(defmulti hello #(do (println "Changed!") %))
(defmethod hello :world ([_] (println "World!")))

;> (hello :world)
Changed!
World!

Be warned there's a trade-off, if you redef the defmulti it removes the defmethods and you need to redef those as well, which is why defmulti is normally caching and does not let you redef it.

Upvotes: 1

Rumid
Rumid

Reputation: 1707

According to clojuredocs example defmulti won't allow you to redefine it. You will have to unmap do-something from the namespace(ns):

(ns-unmap *ns* 'do-something)

and reassign it, as you did before:

(defmulti do-something resolve-it)
(defmethod do-something :do-it [] (println "do-it"))
(defmethod do-something :oh-no [] (println "oh-no"))

Upvotes: 9

ez121sl
ez121sl

Reputation: 2429

There is a simple technique that allows re-defining the dispatch function of the multimethod. The idea is to pass the var that holds the dispatch function to defmulti, not the function itself. Note the #'resolve-it in defmulti instead of just resolve-it. Thus the var is de-referenced at runtime, not just at compile time.

(defn resolve-it [] :do-it)
(resolve-it) ;; :do-it, as expected

(defmulti do-something #'resolve-it)

(defmethod do-something :do-it [] (println "do-it"))
(defmethod do-something :oh-no [] (println "oh-no"))

(do-something) ;; "do-it", as expected

;; now change resolve-it
(defn resolve-it [] :oh-no)
(resolve-it) ;; :oh-no, as expected

(do-something) ;; "oh-no", expected!!

Upvotes: 13

Alan Thompson
Alan Thompson

Reputation: 29966

It looks like defmulti is caching the dispatch function. Here is a modified version of your code that illustrates the problem:

;; simple fn to resolve defmethod to call, hardcoded to :do-it
(defn who-is-it [person] (:name person))
(spyx (who-is-it {:name :joe}))

(defmulti  do-something who-is-it)
(defmethod do-something :homer [person] :doh)
(defmethod do-something :bill  [person] :oh-no)
(defmethod do-something :ted  [person] :excellent)

(spyx (do-something {:name :homer}))
(spyx (do-something {:name :bill}))

;; now change who-is-it
(defn who-is-it [arg] :ted)
(spyx (who-is-it :wilma)) ;; expected result = :excellent
(spyx (do-something {:name :betty}))

with results:

:reloading (tst.clj.core)
(who-is-it {:name :joe}) => :joe
(do-something {:name :homer}) => :doh
(do-something {:name :bill}) => :oh-no
(who-is-it :wilma) => :ted
:error-while-loading tst.clj.core

Error refreshing environment: java.lang.IllegalArgumentException: No method in multimethod 'do-something' for dispatch value: :betty, compiling:(tst/clj/core.clj:22:27)

It looks like you may need to reinitialize the REPL to redefine the dispatch fn. Even repeating everything did not overwrite do-something for me:

(defmulti  do-something who-is-it)
(defmethod do-something :homer [person] :doh)
(defmethod do-something :bill  [person] :oh-no)
(defmethod do-something :ted  [person] :excellent)

(spyx (do-something {:name :betty}))   ;=> ***same error ***
Error refreshing environment: java.lang.IllegalArgumentException: No method in multimethod 'do-something' for dispatch value: :betty, compiling:(tst/clj/core.clj:30:1)

Here with a new session we see the expected behavior:

;; simple fn to resolve defmethod to call, hardcoded to :do-it
(defn who-is-it [person] (:name person))
(spyx (who-is-it {:name :joe}))

;; now change who-is-it
(defn who-is-it [arg] :ted)
(spyx (who-is-it :wilma)) ;; expected result = :ted
; (spyx (do-something {:name :betty}))

(defmulti  do-something who-is-it)
(defmethod do-something :homer [person] :doh)
(defmethod do-something :bill  [person] :oh-no)
(defmethod do-something :ted  [person] :excellent)

(dotest
  (spyx (do-something {:name :betty})))

(do-something {:name :betty}) => :excellent  ; *** as expected ***

Update

I tried the ns-unmap technique Rumid described and it works also. I noticed you have to reissue both the defmulti and all of the defmethod statements:

(ns-unmap *ns* 'do-something)    ; be sure to remember the quote
(defmulti  do-something who-is-it)
(defmethod do-something :homer [person] :doh)
(defmethod do-something :bill  [person] :oh-no)
(defmethod do-something :ted  [person] :excellent)

(dotest
  (newline)
  (spyx (do-something {:name :betty}))) ;=> :excellent

Upvotes: 3

Related Questions