mainly, i just hope that whatever OO Clojurians finally coalesce
towards doesn't make the same mistakes that have already been made by
plenty of other languages, from old C++ through to new Scala. (not
that i have the Programming Language Theory chops to help much there,
myself.)
sincerely.
> I personally like Haskell's philosophy in this area: there is a
> facility for defining data layouts, and there is a facility for
> defining protocols, and they are completely separate. You can define a
I like that as well, and I have been trying to do something similar
in Clojure with my types module (clojure.contrib.types) and my
collection of generic interfaces (clojure.contrib.generic). It's too
early to say if these will work out well, but I do think they are as
close to Haskell's philosophy as possible in a dynamically typed
language.
I haven't thought much about extending types yet. It could mean
opening the can of worms associated with inheritance and all that. I
am waiting for a concrete situation where extension would be useful
to think about how best to do it.
> Clojure's built-in defstruct provides a way to specify record types,
> but not to extend them.
It is not really meant to define types, the documentation clearly
says that struct maps are just an implementation of maps optimized
for a specific frequent use case.
> (I have found Clojure MultiFns awkward to use thus far, especially
> when trying to design extensible APIs around them; client code has to
> know too much about the implementation details of MultiFns--e.g. you
> have to know when and how to hand-tweak dispatching for the particular
> MultiFns you are given).
Could you elaborate a bit on this? I haven't met any major obstacles
with multimethods yet. The dispatch functions give quite a lot of
flexibility in practice. In what situation did you find them
inconvenient?
Konrad.
To summarize, I think the points that have been raised here and on
related threads are that:
1. Structs don't inherently have a type. If you want to dispatch on
type (a common use-case), you have to make a constructor that inserts
the type information as part of the struct. Some have expressed
concern that it may be too easy for this "type information" to be
altered, or worse, the data could be "changed" or "removed" in a way
that makes the struct inconsistent with the type label it carries
around.
2. No way to call "next-method" or "super", which limits the ability
to reuse related methods.
3. The dispatch mechanism requires a lot of explicit prefer-methods,
or else it may be hard to guarantee you won't get a run-time error
from a situation the dispatch system considers ambiguous. This also
makes code less extensible because to add a method, you must know
about all the other methods that have been implemented in order to
insert all the appropriate preferences.
Honestly, the one time I used Clojure's multimethods (contrib.math),
they suited my needs perfectly, but I definitely see where these
concerns are coming from. The existing mechanism is very flexible in
one respect: you can dispatch on anything you want, not just type. In
other respects, it seems like Clojure's multimethods are less
sophisticated than their CLOS/Dylan counterparts. If there is a way
to achieve the same things in Clojure, it's not readily apparent (see
recent thread about the next-method/super issue, for example).
> 1. Structs don't inherently have a type. If you want to dispatch on
> type (a common use-case), you have to make a constructor that inserts
> the type information as part of the struct. Some have expressed
And/or in the metadata.
> concern that it may be too easy for this "type information" to be
> altered, or worse, the data could be "changed" or "removed" in a way
> that makes the struct inconsistent with the type label it carries
> around.
I am one of those who have expressed concerns, but I must also say
that until now I have not encountered such a problem in real life. I
have come to the conclusion that interfaces and types work
differently in Clojure than in other languages that most of us are
more familiar with. So perhaps we are expecting problems that really
aren't there once you figure out how to do it "right".
> 2. No way to call "next-method" or "super", which limits the ability
> to reuse related methods.
Again, I'd like to see a real-life situation where this is an issue.
> 3. The dispatch mechanism requires a lot of explicit prefer-methods,
> or else it may be hard to guarantee you won't get a run-time error
> from a situation the dispatch system considers ambiguous. This also
And there as well.
> makes code less extensible because to add a method, you must know
> about all the other methods that have been implemented in order to
> insert all the appropriate preferences.
This would only be an issue in complex hierarchies. As long as each
library just adds its types to a hierarchy and implements methods for
them, there is no problem. That's why I'd like to see the real-life
situation where there is one.
Konrad.
> Plus the inability to dispatch on other than class or eql, the
> inability to superimpose another taxonomy without redefining the
> class, the inability to have multiple independent taxonomies...
The ability to have multiple taxonomies is indeed very useful in my
(limited) experience.
> One is left-to-right argument order precedence, which roughly
> translates to vector precedence in Clojure (although the mapping to a
> vector need not follow arg order).
I am not sure I'd want such a precedence. If I dispatch on several
arguments, their role is sometimes symmetric, sometimes not, and
where it is not, it is not necessarily the first argument that takes
precedence.
However, what I wished I had on some occasions is a way to specify a
sequence of dispatching values as a result of the dispatch function.
For example, in a two-argument dispatch, I'd like to be able to write
something like
(defmulti foo
(fn [x y]
(let [tx (type x) ty (type y)]
(list [tx ty] (cond (isa? tx ty) ty (isa? ty tx) tx :else #{tx
ty})))
with the result of calling a method for [tx ty] if available and one
of tx/ty/#{tx ty} otherwise. Without such an option, I sometimes have
to write several methods with an identical implementation but
different dispatch signatures.
One way to make this and other generalizations possible would be to
have access to the multimethod's implementation list (and perhaps
preference order) in the dispatch function.
> What I like about preference declarations is that you are
> acknowledging a definite ambiguity in a pure derivation hierarchy
> rather than poisoning the notion of hierarchy with directionality.
What I like about them is that an ambiguity often points to a design
problem that should better be fixed than circumvented.
> That said, perhaps the preference specifications at the method level
> aren't as reusable/convenient as preferences declared at another level
> or somewhere else.
How about a preference function on each multimethod, along with the
dispatch function? One could then easily use one such function for
several multimethods. But what I suggested above (access to the
dispatch signatures and preferences in the dispatch function) should
be even more general.
Konrad.
Konrad.
In Rich's list of ideals, I particularly appreciate the desire to be
able impose a parent on a child without changing the child. This has
come up for me, and I was grateful Clojure had this ability.
I didn't see anything in the list of ideals that explains the lack of
a next-method, and it's not really clear to me how to reap some of the
benefits of inheritance without this.
On a somewhat related note, as my code grows, and I'm using more
complicated data structures, representing all of them as hash tables
is starting to feel a little too, well, unstructured. Can anyone else
report from the Clojure trenches on ways to manage this complexity?
a b
| |
----------
|
c
user> (defmulti test-prefer :tag)
#'user/test-prefer
user> (defmethod test-prefer ::a [h] "a")
#<MultiFn clojure.lang.MultiFn@6551c1>
user> (defmethod test-prefer ::b [h] "b")
#<MultiFn clojure.lang.MultiFn@6551c1>
user> (derive ::c ::a)
nil
user> (derive ::c ::b)
nil
user> (test-prefer {:tag ::a})
"a"
user> (test-prefer {:tag ::b})
"b"
user> (test-prefer {:tag ::c})
; Evaluation aborted.
user> (prefer-method test-prefer ::a ::b)
#<MultiFn clojure.lang.MultiFn@6551c1>
user> (test-prefer {:tag ::c})
"a"
So if I understand correctly, the crux of the problem is that this
preference information is not part of the actual derive hierarchy.
This gives added flexibility to customize on a method by method basis,
but at a cost.
One cost seems to be that if an outside user wants to write a new
method on this very same type hierarchy, he can only do it if he knows
how the types are derived from one another inside the library so that
he can restate all the necessary precedences with appropriate
prefer-method calls on his own new method.
Another interesting question is whether at least test-prefer is safe
for extension. Here's a potential problem I see. Let's say that in
the above example, type ::c is what is intended to be publicly
visible, and the base classes ::a or ::b just provide the base
implementation for various methods, including prefer-method.
I may come along and want to extend test-prefer to a type ::d which
derives from ::c and ::e, where ::e provides an alternative
implementation.
a b (a and b are intended to be hidden from end-user)
| |
----------
|
c e
| |
------------
|
d
(derive ::d ::c)
(derive ::d ::e)
(defmethod test-prefer ::e [h] "e")
Now, as an external user, I know nothing about where test-prefer on
::c gets its behavior. Obviously, I have to disambiguate between
whether test-prefer chooses ::c over ::e, so I may try something like
this:
(prefer-method test-prefer ::c ::e)
But this will not work. I still get an error saying I need to
disambiguate between ::a and ::e. And not knowing anything about ::a,
I could be very confused, and not know how to provide this
information.
Considering how complex the situation can get with single dispatch, I
imagine it gets even more complex in multiple dispatch situations. In
the multimethods example at clojure.org/multimethods, an example is
given of disambiguating between [::shape ::rect] and [::rect ::shape].
But again, if you're writing the bar method from outside of the
library which defines ::shape and ::rect, you might not even know
which is more specific. It might be easier to choose a strategy, such
as more specific on the leftmost taking precedence, than to know the
details of how the various types in the library interact.
Anyway, this is my attempt to digest and restate what I've learned
from this thread and from tinkering around. Any other good examples
that illustrate some of the difficulties?
> I think I've answered at least part of my own question. This is the
> simplest ambiguous case I've found:
>
> a b
> | |
> ----------
> |
> c
Indeed. An ambiguous situation can occur whenever the type hierarchy
graph is not a tree. Since it takes at least three nodes to make a
graph that is not a tree, your example is the simplest one possible.
> So if I understand correctly, the crux of the problem is that this
> preference information is not part of the actual derive hierarchy.
That is one way to put it, but I think there are other possible
solutions than putting the preference information into the hierarchy.
> One cost seems to be that if an outside user wants to write a new
> method on this very same type hierarchy, he can only do it if he knows
> how the types are derived from one another inside the library so that
> he can restate all the necessary precedences with appropriate
> prefer-method calls on his own new method.
That could be avoided with a set of utility functions/macros for
placing types into a hierarchy and for defining multimethods. These
utility functions could take care of adding prefer-method calls as
necessary.
Of course, if this is a frequent need, explicitly language support
would be preferable, but at least that would be a way to experiment
with design options to figure out what works best.
> Now, as an external user, I know nothing about where test-prefer on
> ::c gets its behavior. Obviously, I have to disambiguate between
> whether test-prefer chooses ::c over ::e, so I may try something like
> this:
> (prefer-method test-prefer ::c ::e)
>
> But this will not work. I still get an error saying I need to
> disambiguate between ::a and ::e. And not knowing anything about ::a,
> I could be very confused, and not know how to provide this
> information.
True. It would be nice if prefer-method could itself figure out
that ::a provides the implementation for ::c. But again that
functionality can be added with utility functions, as the hierarchy
can be explored using the functions parents, ancestors, and descendants.
> Considering how complex the situation can get with single dispatch, I
> imagine it gets even more complex in multiple dispatch situations. In
> the multimethods example at clojure.org/multimethods, an example is
> given of disambiguating between [::shape ::rect] and [::rect ::shape].
> But again, if you're writing the bar method from outside of the
> library which defines ::shape and ::rect, you might not even know
> which is more specific. It might be easier to choose a strategy, such
> as more specific on the leftmost taking precedence, than to know the
> details of how the various types in the library interact.
I am not so sure about this. I wonder if there is a real-life use
case where a client library would need to solve dispatching issues on
types in another library about which it doesn't know anything. If
there isn't, the problem is not relevant, and if there is, I'd like
to see a demonstration that something like left-to-right precedence
is indeed a reasonable default.
On the other hand, I would definitely like to be able to implement
left-to-right precedence myself on top of Clojure's multimethods, and
it seems that at the moment this is not possible.
Konrad.
On the other hand, I would definitely like to be able to implement
left-to-right precedence myself on top of Clojure's multimethods, and
it seems that at the moment this is not possible.
There is prefers:
(prefers print-method)
-> {clojure.lang.ISeq #{clojure.lang.IPersistentCollection
java.util.Collection}, clojure.lang.IPersistentList
#{clojure.lang.ISeq}}
> If none of these considerations moves Rich much, that's okay. He gave
> us the tools we need to write our own solutions.
Hang tight - I hear your concerns and am thinking about them.
Rich
I just saw this on Planet Lisp:
http://www.foldr.org/~michaelw/log/programming/lisp/clos
http://www.foldr.org/~michaelw/projects/jclos/
It's a port of Tiny-CLOS to Java by Michael Weber.
--
Michael Wood <esio...@gmail.com>
So, at minimum, to make a solid port, you need to add a function that
can return a sensible type value for any input
(type (proxy [clojure.lang.IMeta clojure.lang.IRef][]))
java.lang.UnsupportedOperationException: meta (NO_SOURCE_FILE:0)
[Thrown class clojure.lang.Compiler$CompilerException]
No doubt someone is going to point out that the proxy object I created
there is useless; that's true, but beside the point. The point is that
it's straightforward to create some value v for which (type v) is
undefined. In order to make a Clojure-friendly version of CLOS, you
need some concept of object type such that you can define a function
that returns a sensible type for any value.
Am 29.03.2009 um 08:34 schrieb David Nolen:
> Not totally following you here as:
>
> (proxy [clojure.lang.IMeta clojure.lang.IRef][])
>
> immediately throws an error. I can't think of a situation in Clojure
> where the type function does not return a usable value. Let me know
> if I'm wrong, but your example is not a case as far as I can tell.
The point that Mikel wants to make is, that you can
easily provide a proxy, which implements clojure.lang.IMeta.
But when you don't provide a "meta" implementation,
the proxy will throw a UnsupportedOperation exception.
Hence type will not work.
I personally consider this a non-issue. What is the point
of providing an interface and then yell "I WONT DO IT" at
the system, when it tries to do the advertised action? In
my opinion, you should provide the advertised methods.
What's the point of the interface otherwise?
Sincerely
Meikel
Am 27.03.2009 um 09:25 schrieb Mark Engelberg:
> I may come along and want to extend test-prefer to a type ::d which
> derives from ::c and ::e, where ::e provides an alternative
> implementation.
>
> a b (a and b are intended to be hidden from
> end-user)
> | |
> ----------
> |
> c e
> | |
> ------------
> |
> d
>
>
> (derive ::d ::c)
> (derive ::d ::e)
> (defmethod test-prefer ::e [h] "e")
>
> Now, as an external user, I know nothing about where test-prefer on
> ::c gets its behavior. Obviously, I have to disambiguate between
> whether test-prefer chooses ::c over ::e, so I may try something like
> this:
> (prefer-method test-prefer ::c ::e)
>
> But this will not work. I still get an error saying I need to
> disambiguate between ::a and ::e. And not knowing anything about ::a,
> I could be very confused, and not know how to provide this
> information.
Is there some special reason, why choosing ::c is not enough in
prefer-method? As soon as I preferred ::c over ::e. Why should
I then need to go further up the tree?
Sincerely
Meikel
>> Enjoying the thread. Out of curiosity for which Clojure values is
>> the return
>> value of the type function undefined?
>
> Try this:
>
> (type (proxy [clojure.lang.IMeta clojure.lang.IRef][]))
>
> java.lang.UnsupportedOperationException: meta (NO_SOURCE_FILE:0)
> [Thrown class clojure.lang.Compiler$CompilerException]
>
>
> No doubt someone is going to point out that the proxy object I created
> there is useless; that's true, but beside the point.
Not entirely. Your example object is not only useless, it is as close
as possible to an object created intentionally to cause trouble. You
create an object that derives from IMeta, thus claiming that it
handles metadata, but then don't provide an implementation that would
actually make metadata work.
BTW, the function type could easily be fixed to handle your problem.
At the moment it does
(or (:type (meta x)) (class x))
Adding an exception handler that returns (class x) whenever (meta x)
fails would take care of your pathological object. I can't say if
this would add much runtime overhead, not being much of a JVM expert.
Konrad.
> Sure, that's right. Maybe constructing such a value in the first place
> is an error.
I'd say so. If it were up to me to provide a fix for the situation
you describe, I'd fix proxy to make it impossible to create an object
that doesn't implement the interfaces it claims to implement.
However, it is well possible that such a fix would be difficult,
impossible, or imply a high run-time penalty. I don't know enough
about the JVM to judge.
However, I consider this case sufficiently pathological, and highly
unlikely to occur by mistake, that I'd accept functions raising
exceptions when presented with such an inconsistent object. In other
words, I'd declare such objects "not a valid Clojure object", without
necessarily enforcing that rule at object creation time.
Konrad.