As a first stab, I'd use an atom holding a list of dispatch-fn,
map-of-argfn-return-values-to-methods, default-method
Something like:
(defmacro defmethod [name dfn]
`(def ~name (atom
[~dfn
{}
(fn [& args#]
(throw (IllegalArgumentException. (str "no dispatch for " args#))))]))
(defn add-method [name dfn-val meth]
(swap! name assoc-in [1 dfn-val] meth))
(defmacro addmethod [name dfn-val bindings & body]
`(add-method ~name ~dfn-val
(fn ~bindings ~@body)))
(defn set-method-default-meth [name meth]
(swap! name assoc 2 meth))
(defmacro setmethod-defaultmeth [name bindings & body]
`(set-method-default-meth ~name
(fn ~bindings ~@body)))
(defn call-method [name & args]
(let [a @name]
(apply (get-in a [1 (apply (a 0) args)] (a 2)) args)))
This doesn't bind the method name itself to a callable function; you
have to (call-method name args ...) rather than (name args ...). It's
also untested. But it should give you some idea of how something like
this can be implemented.
The default method is called if the dispatch function's return value
isn't found in the map. The default default method is the IAE throw in
the first macro. Methods can be replaced by doing a fresh addmethod
with the same dispatch value, but I didn't bother to include a
deleter. It should be simple enough to implement with dissoc. (It's a
shame there isn't a dissoc-in. Oh, wait, you can easily write your
own:
(defn dissoc-in
"Removes an entry in a nested associative structure.
(= (dissoc-in {:a {:b 1 :c 2} :d {:e 3 :f 4}} [:a :c])
{:a {:b 1} :d {:e 3 :f 4}})"
([m keys]
(if (= 1 (count keys))
(dissoc m (first keys))
(let [ks (butlast keys)
k (last keys)]
(assoc-in m ks (dissoc (get-in m ks) k))))))
(this one IS tested).)
Making the thing work with (name args ...) is not too too difficult.
You'd have to have defmethod output both a def of an atom like above,
but with a gensym for a name, and a defn with the specified name that
has the body of call-method, more or less, but with the name arg fixed
to the gensym. There'd also need to be a global names-to-gensyms table
somewhere to make addmethod and the like work.
This variation looks like (defmacro defmethod [name dispatch-fn] `(do
(def ...) (defn ~name ...))).
Another alternative is for defmethod to def the name to a function
that closes over the atom and, via special sentinel arguments,
implements all the functionality of addmethod etc. as well as
call-method. When called without sentinel arguments it does a normal
call-method; when called as (name :add dispatch-val meth) it adds a
method; etc.
This variation looks like (defmacro defmethod [name dispatch-fn] `(def
~name (let [a (atom ...)] (fn ...)))).
I suppose you are Unlogic from IRC. I don't whether you saw it, but I posted some rough sketch: http://paste.pocoo.org/show/303462/
It just introduces the function binding, no other global objects are introduced. The methods are stored in a map in an atom in the metadata of the Var of the multimethod. I haven't tested things, though. Implementing isa? dispatch and other sugar is left as an excercise to the astute reader. ;)
Sincerely
Meikel
PS: atom may have metadata! For comparison, see also the metadata on the Var.
user=> (def x (atom 0 :meta {:meta :data}))
#'user/x
user=> (meta x)
{:meta :data}
user=> (meta #'x)
{:ns #<Namespace user>, :name x, :file "NO_SOURCE_PATH", :line 2}
Am 11.12.2010 um 23:10 schrieb Alexander Yakushev:
> Thanks for your response! Your example is very useful, though I wanted
> to implement the multimethods without that multi-call layer, so it
> will look just like an ordinary function. Thanks to Ken Wesson I
> already have an idea how to do this.
I'm a bit confused. It just looks like a normal function call.
(my-defmulti foo type)
(my-defmethod foo String [x] (str "A String: " x))
(foo "Hello, World!")
So it just looks like an ordinary function. Extracting the multi-call function saves code size, eases macro development and allows to change the underlying driver function for all multimethods while working on it. Very helpful, because you don't have to re-call the my-defmulti macro, but still have the changes take immediate effect.
> Oh, that's my fault, I tried with-meta function on the atom and it
> wouldn't work. Still, after I defined an atom with some metadata in
> it, how can I change it thereafter?
I believe, you can't. You have to create a new atom.
Sincerely
Meikel
You can "change" the metadata on the object held by the atom (if that
object supports metadata) via (swap! a with-meta ...).
One thing a bit annoying is if you want to alter the metadata in an
incremental way. To do that atomically requires a closure. Or defining
a swap-meta! function, like so:
(defn swap-meta! [a f & args]
(swap! a
(fn [x]
(with-meta x (apply f (meta x) args)))))
That abstracts the "do it with a closure" method into a single function.
user=> (def x (atom (with-meta [] {:foo 1})))
#'user/x
user=> (meta @x)
{:foo 1}
user=> (swap-meta! x assoc :bar 2)
[]
user=> (meta @x)
{:bar 2, :foo 1}
Not exactly. My swap-meta! is a bit more concise to use. (Where did
you find vary-meta? There seems to be a lot of stuff that's there, but
hardly anyone knows about.)
Am 13.12.2010 um 21:42 schrieb Ken Wesson:
> (Where did
> you find vary-meta? There seems to be a lot of stuff that's there, but
> hardly anyone knows about.)
http://clojure.github.com/clojure/
Hope that helps.
Sincerely
Meikel
That's not what I meant. I figure all of us have tabs permanently open
to there (I have two actually). What we don't have is the whole thing
memorized, or the time to read it all rather than use it for reference
and, sometimes, control-F it for things. In particular, my due
diligence with atoms and metadata consisted of searching for functions
that work directly with atoms. Of course vary-meta isn't one of them.
My question of Alan was really more a matter of how he found it --
what called his attention to it.
I don't think there's actually a serious problem with the
documentation here; people can't memorize the documentation as there's
too much of it, nor read it all in advance for the same reason, and
search on particular topics won't always find something useful that's
not quite directly related to that topic, but there's little that can
be done about it. The documentation can't be made much shorter without
making it incomplete, nor can every search scenario be anticipated in
advance. So, sometimes people will not know about various functions,
especially more obscure ones that are uncommonly used.
The only problem, in other words, and it is a minor one, is when
people post (possibly only implied) criticisms of other people for
happening to not know about one of those things. That will lead to a
fair amount of time being wasted both on posting such criticisms and
on defending against same by pointing out what due diligence one
performed and why it didn't find whatever particular thing.
Note: that last is not directed at you, in particular, Meikel. It was
Alan (notably the only partly-anonymous user in this thread thus far)
that was a bit abrupt and seemed to be suggesting that I had done
something wrong, or at least questionable, and then explicitly
recommended other people ignore my code (despite the fact that it
works perfectly), and most likely he simply either got up on the wrong
side of the bed or didn't quite mean it that way but misstated
something. At the same time, it was also Alan who called our attention
to the existence of the vary-meta function. Make of that what you
will.
Am 13.12.2010 um 23:52 schrieb Ken Wesson:
> That's not what I meant. I figure all of us have tabs permanently open
> to there (I have two actually). What we don't have is the whole thing
> memorized, or the time to read it all rather than use it for reference […]
My solution to this problem is actually quite simple: I took the time to read it. Not all at one day, but slowly function after function. If I saw a function in other people's code, which I didn't know, I looked it up. Or I read through the overview list and thought „WTF does alter-var-root do?“
Then it was either something „Dang! I need this twice a day. Why don't I know about it?“. Then I would certainly not forget it anymore. Otherwise I would at least remember „There was something.“ and know where to search in the documentation to find the details again if needed.
Of course there are also functions, which I constantly forget. for is such a candidate. When it was new I *always* forgot about it. I built complicated mapcat-reduce-map combinations, which where a simple, short for. Even nowadays I have to remind myself about for every now and then. Such things trickle in slowly.
However, this process is called „learning“ and there is no short-cut to it. This process takes time. There is no „I know Kung-Fu.“ in the real world. cf. http://norvig.com/21-days.html
Sincerely
Meikel
One can also apply a measure of common sense. If you're writing a
function that has a general use, such as changing the metadata, you
may very well consider the possibility it has already been written. If
you then open the API page and search for a function that contains
"meta", you'd soon run across vary-meta.
- James
I resent the implication that I didn't.
> If you're writing a function that has a general use, such as changing
> the metadata, you may very well consider the possibility it has already
> been written. If you then open the API page and search for a function
> that contains "meta", you'd soon run across vary-meta.
Yes, but if you look for functions that work on atoms you won't, and
someone looking for functionality like swap-meta! is at least as
likely to think "atoms" as "metadata" when formulating a search.
I don't think there's any point in endlessly rehashing this and trying
to point a finger of blame. Nobody has actually done anything wrong
here, as near as I can tell, OTHER than trying to turn this into a
blame game by getting critical.
Thank you.
> Have confidence and laugh if you think someone is
> disparaging: actions speak far louder than words.
Yeah, perhaps I should. I just worry that the same attitude aimed at
another user might provoke escalation, even a flamewar.