Search code examples
emacsclojureslimemultimethod

Reloading multimethods via Slime


I'm having trouble reloading multimethods when developing in Emacs with a Slime repl.

Redefining the defmethod forms works fine, but if I change the dispatch function I don't seem to be able to reload the defmulti form. I think I specifically added or removed dispatch function parameters.

As a workaround I've been able to ns-unmap the multimethod var, reload the defmulti form, and then reload all the defmethod forms.

Presumably this is a "limitation" of the way Clojure implements multimethods, i.e. we're sacrificing some dynamism for execution speed, but are there any idioms or development practises that help workaround this?


Solution

  • The short answer is that your way of dealing with this is exactly correct. If you find yourself updating a multimethod in order to change the dispatch function particularly frequently, (1) I think that's unusual :-), (2) you could write a suite of functions / macros to help with the reloading. I sketch two untested (!) macros to help with (2) further below.

    Why?

    First, however, a brief discussion of the "why". Dispatch function lookup for a multimethod as currently implemented requires no synchronization -- the dispatch fn is stored in a final field of the MultiFn object. This of course means that you cannot just change the dispatch function for a given multimethod -- you have to recreate the multimethod itself. That, as you point out, necessitates re-registration of all previously defined methods, which is a hassle.

    The current behaviour lets you reload namespaces with defmethod forms in them without losing all your methods at the cost of making it slightly more cumbersome to replace the actual multimethod when that is indeed what you want to do.

    If you really wanted to, the dispatch fn could be changed via reflection, but that has problematic semantics, particularly in multi-threaded scenarios (see Java Language Specification 17.5.3 for information on reflective updates to final fields after construction).

    Hacks (non-reflective)

    One approach to (2) would be to automate re-adding the methods after redefinition with a macro along the lines of (untested)

    (defmacro redefmulti [multifn & defmulti-tail]
      `(let [mt# (methods ~multifn)]
         (ns-unmap (.ns (var ~multifn)) '~multifn)
         (defmulti ~multifn ~@defmulti-tail)
         (doseq [[dispval# meth#] mt#]
           (.addMethod ~multifn dispval# meth#))))
    

    An alternative design would use a macro called, say, with-method-reregistration, taking a seqable of multifn names and a body and promising to reregister the methods after executing the body; here's a sketch (again, untested):

    (defmacro with-method-reregistration [multifns & body]
      `(let [mts# (doall (zipmap ~(map (partial list 'var) multifns)
                                  (map methods ~multifns))))]
         ~@body
         (doseq [[v# mt#] mts#
                 [dispval# meth#] mt#]
           (.addMethod @v# dispval# meth#))))
    

    You'd use it to say (with-method-reregistration [my-multi-1 my-multi-2] (require :reload 'ns1 ns2)). Not sure this is worth the loss of clarity.