Two Guys Arguing

Reloading Multimethods

Posted in clojure by youngnh on 11.05.10

Replacing code in a running Lisp has always been a bit of a black art to me. I got wedged in a weird situation with multimethods today:

I started clojure via slime and my first test and implementation loaded and ran without any problems:

(ns lambda-reductions)

(deftest test-substitute
  (testing "variable substitution"
    (is (= 'r (substitute 'x ['x 'r])))
    (is (= 'y (substitute 'y ['x 'r])))))

(defmulti substitute (constantly variable))

(defmethod substitute 'variable [form [from to]]
  (if (= form from) to form))

I wrote my code and my tests in the same namespace, as I was just going for speed here.

My second test worked fine too the only real code “replacement” being that I altered my dispatch function and added another method to the multimethod:

(testing "apply substitutions"
  (is (= '(foo r) (substitute '(foo x) ['x 'r]))))

(defmulti substitute (fn [form _]
                   (cond (list? form) 'apply)
             :otherwise 'variable))

(defmethod substitute 'apply [[f & args] replace]
  (cons (substitute f replace) (map #(substitute % replace) args)))

I hit C-c C-k (slime-compile-and-load-file), reran my tests from the repl and they seemed to be picked up.

Then I added number a third case, and for some reason I could not get Clojure to recognize that it had loaded it:

(testing "lambda substitutions"
  (is (= '(fn [x] 42) (substitute '(fn [x] 42) ['x 'r])))
  (is (= '(fn [x] x) (substitute '(fn [x] x) ['x 'r])))
  (is (= '(clojure.core/fn [y] r) (substitute '(fn [y] x) ['x 'r])))
  (is (= '(clojure.core/fn [y] (foo r)) (substitute '(fn [y] (foo x)) ['x 'r]))))

(defmulti substitute (fn [form _]
                   (cond (and (list? form) (= 'fn (first form))) 'lambda
             (list? form) 'apply
             :otherwise 'variable)))

(defmethod substitute 'lambda [[_ [arg] t :as form] [from to :as replace]]
  (if-not (= arg from)
    `(fn [~arg] ~(substitute t replace))

My tests failed. Which is a fair enough response when I’ve written my code wrong, but after tweaking things enough times, I was convinced I had the impl right. I C-c C-k‘d a couple more times, just to be sure and then tried a bunch of things to verify that Clojure had loaded the code I asked it to. By every indication, it had.
I used the methods function. (keys (methods substitute)) showed that my dispatch value had been registered.
I used the get-method function. ((get-method substitute 'lambda) '(foo [x] (foo x)) ['x 'r]) returned the expected result. Clojure knew all about my method and the function itself was right. For some reason the dispatcher wasn’t sending my stuff to the right place (a small lesson learned: I’m going to define my dispatch functions as top-level fns instead of anonymous fns in the defmulti form, to make them more testable).

I tried remove-method on each variation in turn, and then C-c C-k‘d again to try and re-add them. No joy.
I tried remove-all-methods, thinking perhaps I’d been too gentle before. Another C-c C-k and my tests still failed.

So I did what every programmer finally does when the magic in the machine stops responding to their incantations, I killed Clojure. M-x slime-quit-lisp and then restarted. A single C-c C-k and re-running my tests showed them all passing. A remove-ns might’ve saved me from having to quit my running lisp altogether, but I’m not sure. I’m still not sure what happened to cause the behavior in the first place.

If you know what I did to get on the bad side of multimethods, chime-in in the comments below.


3 Responses

Subscribe to comments with RSS.

  1. zahardzhan said, on 11.05.10 at 5:54 pm

    Use M-. in Emacs to see what going under the hood. Try to redifine variable binded to multimethod with def. Like (defmulti method (fn [] …)) and then reset with (def method).

    (defmacro defmulti
    “Creates a new multimethod with the associated dispatch function.
    The docstring and attribute-map are optional.

    Options are key-value pairs and may be one of:
    :default the default dispatch value, defaults to :default
    :hierarchy the isa? hierarchy to use for dispatching
    defaults to the global hierarchy”
    {:arglists ‘([name docstring? attr-map? dispatch-fn & options])
    :added “1.0”}
    [mm-name & options]
    (let [docstring (if (string? (first options))
    (first options)
    options (if (string? (first options))
    (next options)
    m (if (map? (first options))
    (first options)
    options (if (map? (first options))
    (next options)
    dispatch-fn (first options)
    options (next options)
    m (assoc m :tag ‘clojure.lang.MultiFn)
    m (if docstring
    (assoc m :doc docstring)
    m (if (meta mm-name)
    (conj (meta mm-name) m)
    (when (= (count options) 1)
    (throw (Exception. “The syntax for defmulti has changed. Example: (defmulti name dispatch-fn :default dispatch-value)”)))
    (let [options (apply hash-map options)
    default (get options :default :default)
    hierarchy (get options :hierarchy #’global-hierarchy)]
    `(let [v# (def ~mm-name)]
    (when-not (and (.hasRoot v#) (instance? clojure.lang.MultiFn (deref v#))) <<<<<< PROBLEM IS HERE
    (def ~(with-meta mm-name m)
    (new clojure.lang.MultiFn ~(name mm-name) ~dispatch-fn ~default ~hierarchy)))))))

  2. s said, on 11.05.10 at 9:30 pm

    tl;dr my guess stupid change in 1.2 with multimethods, possibly old dispatch function hanging around

  3. Meikel said, on 11.08.10 at 7:56 am

    To elaborate a bit more: the “problem” is that redefing the multimethod makes you loose all your methods defined with it. You change and reload the namespace containing the defmulti form. Then suddenly you have to reload also namespaces depending on it. In order to allow such reloading actions, defmulti got defonce semantics. To change the dispatch function of the multimethod you have to unmap it first: (ns-unmap ‘lambda-reductions ‘substitute).

    Hope that helps.

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )


Connecting to %s