When you only have spheres, algebraic types are nice and dandy, but if you
had a plethora of different objects to render, some of them special cases
of others, perhaps using OOP and organizing them into a class hierarchy
would be better?
This type of question is hard to answer without concrete implementations
to examine. This time you are lucky - you can examine the source code
of the ICFP 2000 programming contest entries.
<http://www.cs.cornell.edu/icfp/contest_results.htm>
--
Jens Axel Søgaard
What conclusions can you possibly reach by doing that?
> When you only have spheres, algebraic types are nice and dandy, but if you
> had a plethora of different objects to render, some of them special cases
> of others, perhaps using OOP and organizing them into a class hierarchy
> would be better?
Maybe, but when in need of binary methods you will have to make one or more
messes - and end up using the visitor pattern which I don't find
particularly pleasant. What's the functional equivalent to the visitor
pattern, and to the class hierarchy which is so often used as an example in
OOP textbooks?
I only had to do such a thing once, and used ocaml polymorphic variants -
and I don't remember exactly what my solution looked like, nor if I liked
my solution or not, but I think it was the visitor pattern implemented as a
match over polymorphic variants (representing the "extensible" ADT of
shapes), which would be IMHO the clever brother of the usual thing :)
What should be the functional way to implement a hierarchy like
shape
circle <: shape
box <: shape
and the function "intersect" which is specialized in the case (box,box) and
(circle,circle), but maybe also on (circle,box)?
What should be the functional way to allow extensibility and successive
refinement of the intersect function?
Maybe there are elegant solutions only using ADTs, but maybe one needs
functors, or polymorphic variants, or type classes, existential types etc.
Is there anyone willing to propose solutions?
bye and thanks
Vincenzo
--
Please note that I do not read the e-mail address used in the from field but
I read vincenzo_ml at yahoo dot it
Attenzione: non leggo l'indirizzo di posta usato nel campo from, ma leggo
vincenzo_ml at yahoo dot it
Not a ray tracer, but I'm working on a simple scene graph in OCaml for
OpenGL. To represent visual objects I use polymorphic variants.
Algebraic types (Variants) in OCaml cannot be extended easily, however
polymorphic variants do the job nicely.
The only problem is look up is O(n) in the number of variants if you
rely on a global rendering "dispatch" function. I assumed that was too
much of a hit, so each node carries the render function it needs with
it. This isn't a big space hit as it may sound, because it ends up just
as aliases to the same functions.
If I knew how to hash polymorphic variants generically, a table driven
approach may work and be easily extensible (just add new render
functions to the table) but am leaving that until later.
Objects are used for the rendering context and serve a different purpose
there.
Chris
Well - you asked which of the algebraic datatypes or the OO hierarchy
is "better". In my view the "better" approach is the one that produces
the "cleanest" and "easiest to understand" programs; and to figure
that out requires comparing some concrete programs.
--
Jens Axel Søgaard
That's true. But the contest entries implement different algorithms. You
can't compare the programs side-to-side.
One could try to say: look, OCamlers used algebraic datatypes and they beat
C++ers who used classes. But there is too much variation in the size of the
teams and the number of entries (two single-man C++ teams for 3rd tier? vs
7-man OCaml gangs - even discounting any prior experience in ray tracing
that's a very big difference)
There are a few problems that I see in your post:
1. We are not discussing functional vs imperative, but algebraic datatypes
vs inheritance
2. In the raytracer, "intersect" works on (ray, shape) pairs not (shape,
shape), so the visitor pattern is irrelevant
Regardless
3. Some OOP systems don't need the visitor pattern, because they have
http://en.wikipedia.org/wiki/Multiple_dispatch
And also
4. Abruptly changing "Subject:", while the subject remains the same, is not
very helpful.
5. The ray tracer was actively discussed lately in both clf and cljp - Jon
started the discussion there himself. I'd like to hear what both
communities have to say about this. Repeating the same arguments separatly
in different groups is a waste of time. Don't trim the groups list.
This seems like a very natural hierarchy for functional data
structures.
My solution (SML):
type circle = ...
type box = ...
datatype shape = Circle of circle | Box of box
fun intersect (Circle c1, Circle c2) = ...
| intersect (Box b1, Box b2) = ...
| intersect (Circle c, Box b) = ...
| intersect (b,c) = intersect (c,b) (* this would be a nice place
for a robust guard system
in SML *)
-- Matt
>
> This seems like a very natural hierarchy for functional data
> structures.
>
> My solution (SML):
>
> type circle = ...
> type box = ...
> datatype shape = Circle of circle | Box of box
>
> fun intersect (Circle c1, Circle c2) = ...
> | intersect (Box b1, Box b2) = ...
> | intersect (Circle c, Box b) = ...
> | intersect (b,c) = intersect (c,b) (* this would be a nice place
> for a robust guard system
> in SML *)
Yes, this is the most natural way (and thanks for providing the first
example), but it's not extensible. Not that I want subclassing, but let's
suppose that we want users to be able to provide new shapes and new cases
for intersect. How should we reason in a non-OO language? I've found for
example this paper
http://www.cs.luc.edu/laufer/papers/mixins03.pdf
but I still have to read it
V.
alex goldman wrote:
> There are a few problems that I see in your post:
>
> 1. We are not discussing functional vs imperative, but algebraic datatypes
> vs inheritance
>
In fact I never used the word "imperative" in my post.
I am replying to your topic, which seemed to me: "object-oriented approaches
vs. ADT approaches to problems that arise when writing graphics programs".
I generalized a bit but used a rather imprecise word: "functional", I meant
"ADT plus newer but still ADT-related techniques, like polymorphic
variants, modules, functors, existential ADTs and so on".
> 2. In the raytracer, "intersect" works on (ray, shape) pairs not (shape,
> shape), so the visitor pattern is irrelevant
>
> Regardless
I tried to interpret your intentions and talk about object-oriented
hierarchies and their equivalent in functional languages. I was wrong it
seems, as you only want to talk about Jon raytracer - should I be sorry for
having replied? If so, you should be sorry for having asked in the first
place <G>.
>
> 3. Some OOP systems don't need the visitor pattern, because they have
> http://en.wikipedia.org/wiki/Multiple_dispatch
>
I know, of course! I unintentionally stated that you _need_ the visitor
pattern in OOP. But I guess I can state that using C#, C++, Java, Eiffel,
the O of ocaml and other more or less "mainstream" OO languages, one will
surely think about using the visitor pattern when in need of binary
methods.
> And also
>
> 4. Abruptly changing "Subject:", while the subject remains the same, is
> not very helpful.
>
Seems like you felt the change of subject as a critique of the one you
used...
I changed the subject on purpose, because I was introducing a rather
interesting (at least, to me) subject which I thought be strictly related
to your post (hence the reply) but not strictly related with the original
subject (Jon's raytracer), so I changed the subject hoping that this would
contribute to the signal-to-noise ratio of a possible search on usenet for
the topic.
I see that the subject has much more meaning for readers of cljp than for
readers of clf due to previous discussions, but pardon me I was not aware
of those, so I changed the subject as I am used to do when I think it's
worth - and BTW I think discussing on usenet about what the subject of a
certain post should be is even less helpful than changing a subject :)
> 5. The ray tracer was actively discussed lately in both clf and cljp - Jon
> started the discussion there himself. I'd like to hear what both
> communities have to say about this. Repeating the same arguments
> separatly in different groups is a waste of time. Don't trim the groups
> list.
I actually didn't, so what has this to do with the "few problems" in my
post? And also, I still think that my post was perfectly in topic with your
on CLF, I don't see why you're upset with that.
Again - sorry for this other off topic on both NGs - I think this post will
clarify the previous one to everybody.
V.
> Yes, this is the most natural way (and thanks for providing the first
> example), but it's not extensible. Not that I want subclassing, but let's
> suppose that we want users to be able to provide new shapes and new cases
> for intersect. How should we reason in a non-OO language? I've found for
> example this paper
>
> http://www.cs.luc.edu/laufer/papers/mixins03.pdf
>
> but I still have to read it
Mixins immediately make me thought of Polymorphic Variant in ocaml, and
the mixin.ml files in the testlabl/ sub-directory of the ocaml
compiler source. You could also look at this. See:
http://camlcvs.inria.fr/cgi-bin/cvsweb/ocaml/testlabl/mixin.ml?rev=1.2.6.1;content-type=text%2Fplain
--
Rémi Vanicat
Wow, I'm famous. ;-)
> When you only have spheres, algebraic types are nice and dandy, but if you
> had a plethora of different objects to render, some of them special cases
> of others, perhaps using OOP and organizing them into a class hierarchy
> would be better?
Definitely not in my experience. Time for a shameless plug: The latest
commercial product from my company, Presenta, is a slideshow presentation
program designed to display technical content including animated slideshow
points, typeset mathematics and 2D and 3D graphics:
http://www.ffconsultancy.com/products/presenta
Presenta is now written entirely in OCaml. Two years ago, it was written
entirely in C++. The OCaml implementation makes very little use of objects
(there is a single object type, for a "scene", which was done only to
circumvent having mutually recursive modules).
The internals are very complicated (much more complicated than anything I
did during my PhD in computational physics, for example) and the OCaml
implementation relies heavily upon variant types and pattern matching to
get its jobs done. For example, variant types are used to represent:
1. A whole document.
2. Typeset mathematics.
3. Graphics.
4. The scene graph used for rendering.
5. Lines and Bezier curves.
6. Multiresolution paths.
... and many other entities.
In the case of 2D vector graphics, the program uses a particularly
sophisticated approach to adaptively tesselate geometry in order to render
only what is necessary for the current frame of animation whilst maximally
reusing the results of previous computations and also exploiting hardware
acceleration via OpenGL. This is not easy. Indeed, nobody else has managed
to do this AFAIK.
Having translated this from an entirely OO C++ implementation I can
definitely say that OO is very poorly suited to this application. OCaml's
variants and pattern matching are not only vastly more succinct, they are
also much clearer, much more robust and even much faster than the
equivalent C++. Pattern matching leads to most code updates being localised
where as OO leads to scattered updates which are not statically checked to
the same extent by the compiler (C++ or Java).
Finally, the advantages offered by OCaml don't just lead to 1/2 or 1/3 as
much code, as my ray tracer might lead you to believe. The code density in
the current implementation is more than 5x that of the old C++. For larger
projects, OCaml code is even smaller in proportion. I believe this is due
to the extra dimension of factoring made possible by higher-order
functions.
--
Dr Jon D. Harrop, Flying Frog Consultancy Ltd.
http://www.ffconsultancy.com
When learning a new paradigm, you should not try to translate old solutions
but, rather, you should try to reimplement old problems. So don't ask what
the "visitor pattern" looks like in a functional implementation. Find a
representative example which has been solved using the visitor pattern.
Forget about the visitor pattern. Then try to solve the same problem in ML.
> What should be the functional way to implement a hierarchy like
>
> shape
> circle <: shape
> box <: shape
type shape = Circle of vec * float | Box of vec * vec
> and the function "intersect" which is specialized in the case (box,box)
> and (circle,circle), but maybe also on (circle,box)?
Simply:
let intersect s1 s2 = match s1, s2 with
Circle (c1, r1), Circle (c2, r2) -> ...
| Circle (c, r), Box (l, u)
| Box (l, u), Circle (c, r) -> ...
| Circle (c1, r1), Circle (c2, r2) -> ...
> What should be the functional way to allow extensibility and successive
> refinement of the intersect function?
There are two different ways to extend this problem:
1. You could add a new shape.
In ML, you do this:
type shape =
Circle of vec * float
| Box of vec * vec
| Polygon of vec list
and the compiler will then tell you exactly where you have pattern matches
which don't handle Polygon.
In C++ or Java, you add a new derived class called Polygon:
class Polygon : public Shape {
...
}
2. You could add a new function which acts upon shapes.
In ML, you do this:
let bound = function
Circle of (c, r) -> ...
| Box of (l, u) -> ...
In C++ or Java, you add a new member function to shape:
class Shape {
...
virtual Bound bound() const = 0;
}
and implementations in every derived class:
class Circle : public Shape {
...
virtual Bound bound() const {
...
}
}
In general, a program has far more functions than types. So the ML approach
is better because code updates are localised when adding new functions
(which is more common) than when adding new types (less common). The ML
also has many other advantages. For example, you have to cut and paste the
types a lot in C++ and Java but ML infers all of the types.
Finally, MLs pattern matches are so much more expressive than derived
classes in C++ and Java (e.g. you can pattern match over strings) that the
compiler has a much better chance of optimising them. So ML programs are
typically much faster too.
> (there is a single object type, for a "scene", which was done only to
> circumvent having mutually recursive modules).
Aha!
In an objectless code, your Scene would be a big union of most or all
renderable stuff. If it's an object instead, then everything that's
renderable should be its subtype, no?
> Having translated this from an entirely OO C++ implementation I can
[...]
> The code density in
> the current implementation is more than 5x that of the old C++. For larger
> projects, OCaml code is even smaller in proportion.
My own experience was that simply rewriting an application significantly
reduces its size, even when I rewrite it in the same language.
No, sorry. :-)
I used an object to weaken the type system. Nothing to do with OO and/or
subtyping.
>> The code density in
>> the current implementation is more than 5x that of the old C++. For
>> larger projects, OCaml code is even smaller in proportion.
>
> My own experience was that simply rewriting an application significantly
> reduces its size, even when I rewrite it in the same language.
I have ported several applications from OCaml to C++ and the code expanded
by the same amount, despite being rewritten.
> No, sorry. :-)
>
> I used an object to weaken the type system. Nothing to do with OO and/or
> subtyping.
This is telling us something (about the type system), but not enough to have
a meaningful discussion. Suppose you want to render
Spheres, ellipsoids, cones, planes, parallelepipeds, prisms, etc.
There are obviously complex RELATIONS among these, and you might want to
re-use the code due to these relations instead of just cutting and pasting.
What would your scene _object_ look like?
type aux = Sphere of sphere | Ell of ell | Cone of cone | Plane of ...
class scene = ?
The OCaml type system is deliberately restrictive. Out of 10,000 lines of
code in my latest project, this restrictiveness is advantageous (by
eliminating many errors) in all but one place. In this place, I have use an
object to weaken the type system just enough to let me do what I want.
> Suppose you want to render
>
> Spheres, ellipsoids, cones, planes, parallelepipeds, prisms, etc.
>
> There are obviously complex RELATIONS among these, and you might want to
> re-use the code due to these relations instead of just cutting and
> pasting.
Absolutely. Then you factor your code into many functions. Code reuse is
then achieved by calling one function from many places. For example, if
you're after brevity, you might replace all sphere code with calls to
ellipse.
Your example is be best written in OCaml without using any OO at all.
> What would your scene _object_ look like?
>
> type aux = Sphere of sphere | Ell of ell | Cone of cone | Plane of ...
>
> class scene = ?
My scene _object_ looks nothing like that. It looks like this:
class scene :
?fills:Smoke.Fill.t Store.t ->
?styles:Smoke.Style.geometry Store.t ->
?geometries:Smoke.ContourGeometry.t Store.t ->
basic_node -> object ('a)
method add_fill : Smoke.Fill.t -> Smoke.Fill.t Store.key * 'a
method add_geometry : Smoke.ContourGeometry.t ->
Smoke.ContourGeometry.t Store.key * 'a
method add_style : Smoke.Style.geometry ->
Smoke.Style.geometry Store.key * 'a
method append : basic_node -> int * 'a
method get_bound : Smoke.Bound.t
method get_bound_of : basic_node -> Smoke.Bound.t
method bound_of : iterator -> Smoke.Bound.t
method get_fill : Smoke.Fill.t Store.key -> Smoke.Fill.t
method get_fills : Smoke.Fill.t Store.t
method get_geometry : Smoke.ContourGeometry.t Store.key ->
Smoke.ContourGeometry.t
method get_geometries : Smoke.ContourGeometry.t Store.t
method get_root : basic_node
method get_style : Smoke.Style.geometry Store.key ->
Smoke.Style.geometry
method get_styles : Smoke.Style.geometry Store.t
method push_back : basic_node -> 'a
method push_front : basic_node -> 'a
method render : Smoke.RenderData.t -> unit
method replace_fill : Smoke.Fill.t Store.key -> Smoke.Fill.t -> 'a
method replace_geometry : Smoke.ContourGeometry.t Store.key ->
Smoke.ContourGeometry.t -> 'a
method replace_root : basic_node -> 'a
method replace_style : Smoke.Style.geometry Store.key ->
Smoke.Style.geometry -> 'a
end
It has nothing to do with shapes, geometries and so forth. It is only to do
with the way I chose to structure the whole library. Specifically, it pulls
everything together in a way that can then be used by all of the parts of
my library independently, without having to know about each other. Thus, it
evades a big mutual dependency.
You want to talk about the variant type which implements a scene. I have
chosen to use a variant type (much better than a class hierarchy):
type ('leaf, 'loner, 'group) generic_node =
GenericLeaf of 'leaf
| GenericLoner of 'loner * ('leaf, 'loner, 'group) generic_node
| GenericGroup of 'group * ('leaf, 'loner, 'group) generic_node list
Note that it is both generic and extensible. The functions which act over
this type are also both polymorphic and extensible. I have several levels
of progressively more specialised scenes and functions, allowing the user
to choose how much functionality they want to implement themselves.
This functionality can actually be achieved (albeit unsafely) in C++ and
Java but the code required is so convoluted that nobody would ever think to
write it. In OCaml, the best approach is often the most natural and mose
concise.
--
Dr Jon D Harrop, Flying Frog Consultancy
http://www.ffconsultancy.com
> alex goldman wrote:
>> Jon Harrop wrote:
>>> No, sorry. :-)
>>>
>>> I used an object to weaken the type system. Nothing to do with OO and/or
>>> subtyping.
>>
>> This is telling us something (about the type system), but not enough to
>> have a meaningful discussion.
>
> The OCaml type system is deliberately restrictive. Out of 10,000 lines of
> code in my latest project, this restrictiveness is advantageous (by
> eliminating many errors) in all but one place. In this place, I have use
> an object to weaken the type system just enough to let me do what I want.
okay, but what if you were using just ML (you actually mentioned earlier you
were thinking about porting your book and code to SML). Suppose your code
was in SML from the beginning, nearly 10,000 lines of it. At one point, you
realize that the type system will not allow you to do what you want - your
type checker God tells you to get lost. This is the kind of situation I've
been in with ML (the memories are too painful, and I'm supressing them, so
don't ask, I don't really want to talk about that ;-)
>> Suppose you want to render
>>
>> Spheres, ellipsoids, cones, planes, parallelepipeds, prisms, etc.
>> ...
>> type aux = Sphere of sphere | Ell of ell | Cone of cone | Plane of ...
>>
>> class scene = ?
>
> My scene _object_ looks nothing like that. It looks like this: [...]
I guess I asked the wrong question. What I really wanted to know was what
its relation to spheres, ellipsoids, cones, planes, parallelepipeds,
prisms, etc. looks like.
INRIA folks have a paper about Objects vs not Objects (Modules?). IIRC, the
basic idea is that objects are good when you have few function, but many
data types. What I'm saying is that ray tracing seems just like this type
of application (unless you limit yourself to spheres). There is a couple of
functions (intersect and ?), but very many data types.
So when you use an OO design, the code differences between *Objective* Caml
on the one hand, and Java or C++ on the other hand, will be rather
superficial.
> Jens Axel Søgaard wrote:
>
>
>>alex goldman wrote:
>>
>>>Jens Axel Søgaard wrote:
>>>
>>>>alex goldman wrote:
>>>>
>>>>>Regarding Jon's ray tracer, I'll play the devil's advocate:
>>>>>
>>>>>When you only have spheres, algebraic types are nice and dandy, but if
>>>>>you had a plethora of different objects to render, some of them special
>>>>>cases of others, perhaps using OOP and organizing them into a class
>>>>>hierarchy would be better?
>>>>
>>>>This type of question is hard to answer without concrete implementations
>>>>to examine. This time you are lucky - you can examine the source code
>>>>of the ICFP 2000 programming contest entries.
>>>>
>>>> <http://www.cs.cornell.edu/icfp/contest_results.htm>
>>>
>>>What conclusions can you possibly reach by doing that?
>>
>>Well - you asked which of the algebraic datatypes or the OO hierarchy
>>is "better". In my view the "better" approach is the one that produces
>>the "cleanest" and "easiest to understand" programs; and to figure
>>that out requires comparing some concrete programs.
>
>
> That's true. But the contest entries implement different algorithms. You
> can't compare the programs side-to-side.
The algorithms were not *that* different. The scene language was part of
the contest spec, and ray tracing is a pretty standard activity.
Differences seem to have been due to optimisation, i.e. the
circumstances under which a ray tracer decides that nothing interesting
will happen with a ray anymore. Other than that, I the programs should
be as similar to each other as one could reasonably expect.
> One could try to say: look, OCamlers used algebraic datatypes and they beat
> C++ers who used classes. But there is too much variation in the size of the
> teams and the number of entries (two single-man C++ teams for 3rd tier? vs
> 7-man OCaml gangs - even discounting any prior experience in ray tracing
> that's a very big difference)
The FPL teams generally had no prior experience with ray tracing. (I
dimly recall one of the teams happened to have one person with such a
background.)
Team size difference do make a difference, of course. OTOH that
shouldn't change program structure too much.
Regards,
Jo
Agreed.
> Regardless
>
> 3. Some OOP systems don't need the visitor pattern, because they have
> http://en.wikipedia.org/wiki/Multiple_dispatch
Multiple dispatch just shifts the problems to a different area.
I.e. if programmer A add a new ray type and programmer B adds a new
shape type, they can't do that independently. It's just the same mess as
with the visitor pattern.
Regards,
Jo
> Vincenzo Ciancia wrote:
>> Maybe, but when in need of binary methods you will have to make one or
>> more messes - and end up using the visitor pattern which I don't find
>> particularly pleasant. What's the functional equivalent to the visitor
>> pattern, and to the class hierarchy which is so often used as an example
>> in OOP textbooks?
>
> When learning a new paradigm, you should not try to translate old
> solutions but, rather, you should try to reimplement old problems. So
> don't ask what the "visitor pattern" looks like in a functional
> implementation. Find a representative example which has been solved using
> the visitor pattern. Forget about the visitor pattern. Then try to solve
> the same problem in ML.
>
C'mon, I know :) I mean "what's the functional way to solve problems that
the visitor pattern solves?".
>> What should be the functional way to implement a hierarchy like
>>
>> shape
>> circle <: shape
>> box <: shape
>
> type shape = Circle of vec * float | Box of vec * vec
>
>> and the function "intersect" which is specialized in the case (box,box)
>> and (circle,circle), but maybe also on (circle,box)?
>
> Simply:
>
> let intersect s1 s2 = match s1, s2 with
> Circle (c1, r1), Circle (c2, r2) -> ...
> | Circle (c, r), Box (l, u)
> | Box (l, u), Circle (c, r) -> ...
> | Circle (c1, r1), Circle (c2, r2) -> ...
>
>> What should be the functional way to allow extensibility and successive
>> refinement of the intersect function?
>
> There are two different ways to extend this problem:
>
> 1. You could add a new shape.
>
> In ML, you do this:
>
> type shape =
> Circle of vec * float
> | Box of vec * vec
> | Polygon of vec list
>
> and the compiler will then tell you exactly where you have pattern matches
> which don't handle Polygon.
>
Ok, pretty good, I think this is the way we all do it, and it has the huge
advantage of simplicity. But if you provide this as a library to an user
you (or at least me) would prefer to allow the user to add new shapes
without modifying the original source. For example (it compiles but I don't
know if it really works, however here it is):
(*************************)
type point = (float * float)
module Shapes = struct
type 'a shape =
{ kind : 'a ; (* the kind of shape *)
intersect : 'a shape -> bool; (* an intersection function *)
poly : int -> point list (* calculates an approximation of the
shape of at most n points *) }
let intersect x y = x.intersect y
let generic_intersect poly1 poly2 = false (* stub, poly1 and poly2 are
functions of type
int->point list *)
end
open Shapes
let intersect_circles (c1,r1) (c2,r2) = false (* stub *)
let circle center radius =
let rec res =
{ kind = `Circle (center,radius);
intersect = (fun other -> match other.kind with
`Circle (c2,r2) ->
intersect_circles (center,radius) (c2,r2)
| _ -> generic_intersect res other);
poly = (fun n -> [] (* stub *) )
} in
res
(**************************************)
Now, this is an attempt, but it has many drawbacks. It may be argued it's
complicated, but also the intersect function is not symmetrical (it gives
precedence to the first argument), and also there is a runtime check.
The latter is IMHO the most important point: avoiding that runtime check. I
think this can be done in haskell with type classes, but maybe also in
ocaml. Maybe using classes :)
Regarding the asymmetry of the function maybe it just suffices to add a
third case to the intersect function of e.g. the circle, before the default
one, calling the other intersect function (with care not to generate
endless recursion). However this exposes another very important drawback of
this approach: it's cooperative, in the sense that the user may get it
wrong and make the entire library behave wrongly.
> In general, a program has far more functions than types. So the ML
> approach is better because code updates are localised when adding new
> functions (which is more common) than when adding new types (less common).
> The ML also has many other advantages. For example, you have to cut and
> paste the types a lot in C++ and Java but ML infers all of the types.
>
I agree with this, of course. In fact the solution I wrote above does not
declare any type :)
Any better solution, in ocaml, in SML, haskell or whatever?
bye
> Vincenzo Ciancia wrote:
>> Maybe, but when in need of binary methods you will have to make one or
>> more messes - and end up using the visitor pattern which I don't find
>> particularly pleasant. What's the functional equivalent to the visitor
>> pattern, and to the class hierarchy which is so often used as an example
>> in OOP textbooks?
>
> When learning a new paradigm, you should not try to translate old solutions
> but, rather, you should try to reimplement old problems. So don't ask what
> the "visitor pattern" looks like in a functional implementation.
Why not? It's just a plain recursive function :-)
'In Ankh-Morpork even the shit have a street to itself...
Truly this is a land of opportunity.' - Detritus, Men at Arms
> intersect = (fun other -> match other.kind with
> `Circle (c2,r2) ->
> intersect_circles (center,radius) (c2,r2)
> | _ -> generic_intersect res other);
This is not extensible: if another kind of shape wants a specialized
code for intersecting with circles, it needs to modify previously
written code.
> The latter is IMHO the most important point: avoiding that runtime check.
I disagree. It's a matter of efficiency, not functionality or modularity.
And it's hard to avoid when scenes are read from files or translated from
generic descriptions.
> Regarding the asymmetry of the function maybe it just suffices
> to add a third case to the intersect function of e.g. the circle,
> before the default one, calling the other intersect function
> (with care not to generate endless recursion).
It still requires from two kinds of shapes which need a good
implementation of intersection that one of them knows the other.
I prefer when implementations of intersection can be added after the
respective shapes, so any collection of shape kinds can be completed
by adding suitable implementations.
--
__("< Marcin Kowalczyk
\__/ qrc...@knm.org.pl
^^ http://qrnik.knm.org.pl/~qrczak/
> When learning a new paradigm, you should not try to translate old solutions
> but, rather, you should try to reimplement old problems. So don't ask what
> the "visitor pattern" looks like in a functional implementation. Find a
> representative example which has been solved using the visitor pattern.
> Forget about the visitor pattern. Then try to solve the same problem in ML.
That should be simple. The visitor pattern is just a hack for
languages that doesn't have first class functions - a tuple of
functions implemented as an object. :)
/L
--
Lasse Reichstein Nielsen - l...@hotpop.com
DHTML Death Colors: <URL:http://www.infimum.dk/HTML/rasterTriangleDOM.html>
'Faith without judgement merely degrades the spirit divine.'
>> intersect = (fun other -> match other.kind with
>> `Circle (c2,r2) ->
>> intersect_circles (center,radius) (c2,r2)
>> | _ -> generic_intersect res other);
>
> This is not extensible: if another kind of shape wants a specialized
> code for intersecting with circles, it needs to modify previously
> written code.
Hmm. Suppose you have a circle 'c' and a box 'b'. If one calls 'intersect c
b' the circle version will be called, but if one calls 'intersect b c' the
box version will be called, so my code is wrong (I already knew, I posted
it to contribute to finding a good solution), but the "box" shape can
specialize its intersection with circles. If it has the luck to be called
first!
Now the problem is that you should define "the most specific" method
someway. You could require that each shape declare on what pairs it
specializes, but what would you do if circle specializes the intersection
with boxes, and box specializes the intersection with circles? CLOS solved
in a naive but working way which I am not sure to remember (I studied it
for a couple of months 5 years ago).
I think there is no proper solution to this latter point, however a good
implementation which detects if box has specialized the intersection to
circles even if you call 'intersect circle box' should be found.
Sorry if I overlooked something obvious, of course.
Bye
> I think there is no proper solution to this latter point, however a good
> implementation which detects if box has specialized the intersection to
> circles even if you call 'intersect circle box' should be found.
Um, that's a specialised optimisation that works because intersection is
a commutative (symmetric) operation.
You can't do that in e.g.
subtract circle box (gives a circle with rectangular hole)
subtract box circle (gives a box with a circular hole)
I.e. if you automate the search for the most specific function for the
commutative case, that mechanism needs to know which of the functions
are commutative and which aren't.
Regards,
Jo
Lasse Reichstein Nielsen <l...@hotpop.com> writes:
>> When learning a new paradigm, you should not try to translate old solutions
>> but, rather, you should try to reimplement old problems. So don't ask what
>> the "visitor pattern" looks like in a functional implementation. Find a
>> representative example which has been solved using the visitor pattern.
>> Forget about the visitor pattern. Then try to solve the same problem in ML.
>
> That should be simple. The visitor pattern is just a hack for
> languages that doesn't have first class functions - a tuple of
> functions implemented as an object. :)
Not quite, a tuple of functions is still a translation of a solution
rather than the problem.
It's a hack for extensible classes, i.e. for extending the set of
functions whose implementation is dispatched at runtime on the type
of one of arguments.
It can be used to emulate algebraic types for example (subclasses are
used for constructors, dispatch is used instead of pattern matching).
This can be quite inconvenient:
1. only one layer of constructors is dispatched at a time,
2. you can't have a default case - you have to specify what to do for
all constructors separately,
3. and the body of each match must be moved to a separate method, with
all local variables passed explicitly to it.
In addition
4. it requires a fixed signature of the function, so passing
additional arguments and returning results is tricky (requires
packing multiple arguments in "tuples", which Java lacks anyway,
and casting to overcome static type system).
5. In Java it makes static exception specifications harmful: a single
exception specification must be used for all functions dispatched
that way.
6. It removes the other axis of extensibility, the one which was easy
in plain OOP: adding new types to dispatch on.
Generic functions (like in CLOS and Dylan) solve the problem of adding
dispatched functions without most of these drawbacks (only 1 and 3
remain), and as a bonus they provide dispatching on multiple arguments
at once.
> I.e. if you automate the search for the most specific function for the
> commutative case, that mechanism needs to know which of the functions
> are commutative and which aren't.
I'm happy with not complicating the dispatch semantics by automatic
commutation of some arguments of some operations, because it's easy
to do manually: just whoever defines it on (A,B) provides also a
definition on (B,A).
All the suggested solutions so far seem to have missed or glossed over the
fact that the set of circles and rectangles is not closed under intersection.
To represent the closure of these shapes under intersection, you need
closed poly-arcs (i.e. closed shapes in which each segment is an arc, which
can be straight as a special case). And since circles and rectangles are
just examples of closed poly-arcs, you don't need to represent them as
separate classes or variants; that is just superfluous complication.
You might argue that adding additional kinds of shape would change this
situation. However, look at *how* it changes it: the shape type you need
is always still a kind of closed polyline, but with more general line
segments.
--
David Hopwood <david.nosp...@blueyonder.co.uk>
> To represent the closure of these shapes under intersection, you need
> closed poly-arcs (i.e. closed shapes in which each segment is an arc, which
> can be straight as a special case). And since circles and rectangles are
> just examples of closed poly-arcs, you don't need to represent them as
> separate classes or variants; that is just superfluous complication.
Complication but not necessarily superfluous. I'm certain that
computing the intersection of two rectangles with sides parallel
to axes is faster than doing it for generic polyarcs, so keeping
specific shapes separate might be worth the effort. They need to
be added, they don't have to replace the specific shapes.
So write the code the generic way, and then add the specific shapes
as an optimization, if-and-only-if it turns out that intersecting
rectangles represented as polyarcs is a bottleneck. This has two
advantages:
- there is less code to write before the program works,
- you might never need to do the optimization.
--
David Hopwood <david.nosp...@blueyonder.co.uk>
> Joachim Durchholz <j...@durchholz.org> writes:
>
>> I.e. if you automate the search for the most specific function for
>> the commutative case, that mechanism needs to know which of the
>> functions are commutative and which aren't.
>
> I'm happy with not complicating the dispatch semantics by automatic
> commutation of some arguments of some operations, because it's easy
> to do manually: just whoever defines it on (A,B) provides also a
> definition on (B,A).
That's exactly the consequence I'm drawing myself :-)
Regards,
Jo
What do you mean? You can force the visitor implementor to implement a
default method that can be called with types of shapes.
> It's just the same mess as
> with the visitor pattern.
No, multiple dispatch has a number of advantages over the visitor
pattern. See this page for more info:
http://nice.sourceforge.net/visitor.html
/Jesper Nordenberg
That's one of the ways people try to address the problem.
The base problem is: you have N ray types and M shape types. With any
kind of multiple dispatch (be it the Visitor pattern or built right into
the language), that's a matrix of NxM functions (of course, in practice,
that matrix is filled with relatively few values).
Now if both programmers add a new type, you have an (N+1)x(M+1) matrix.
And it's the [N+1][M+1] corner case that gives us all the trouble:
neither the programmer who's responsible for the N axis nor the
programmer who's responsible for the M axis will feel responsible for
that case - in fact neither knows that this new case exists! And if the
software has an unknown case, there will be nobody who reviews whether
it's handled correctly.
Even worse: adding a new type is an important design change, so it's
quite likely that this new combined case is nontrivially different from
anything that exists.
Forcing a default just covers the problem up by making the system
compile. What's actually needed is the exact opposite: a compiler
message that informs whoever uses the two extensions together that
there's a new, unimplemented case in his system.
Needless to say that this plays havoc with composability: adding two
independent extensions to the system may break it. In other words,
multiple dispatch breaks modularity (the ability to compose independent
libraries without getting adverse interactions).
For the Javaites among the readers: It also prevents dynamic class
loading. Well, one could defer the error message until the
not-provided-for type combination actually turns up and throw an
exception. You'd get multiple dispatch at the price of slightly
destabilising the software... which is actually feasible with the right
overall architecture; see the whitepapers on system stability in Erlang.
>> It's just the same mess as with the visitor pattern.
>
> No, multiple dispatch has a number of advantages over the visitor
> pattern. See this page for more info:
> http://nice.sourceforge.net/visitor.html
OK, I agree MD solves several problems of Visitor.
It doesn't solve the non-modularity problem though. And I don't think
that this is solvable; whatever structure you impose, it will lead to
unexpected results (read: bug opportunities) in some cases.
The best way to handle the problem is to decompose the operation on NxM
types into two: one Nx1, and one 1xM, where the composition will always
do the right thing, without either Visitor or MD. Unfortunately, it
seems that this strategy doesn't always work (but I'm not sure that this
is a fundamental problem - it might be lack of inventiveness that makes
us fail to see how to decompose NxM operations).
Regards,
Jo
> The best way to handle the problem is to decompose the operation on NxM types
> into two: one Nx1, and one 1xM, where the composition will always do the
> right thing, without either Visitor or MD. Unfortunately, it seems that this
> strategy doesn't always work (but I'm not sure that this is a fundamental
> problem - it might be lack of inventiveness that makes us fail to see how to
> decompose NxM operations).
>
Perhaps, but it's not going to go to Nx1 and 1xM - sooner or later that
amounts to assuming the problem's bilinear. Other structures no doubt
exist - I'm having some fun exploring extensible parsing for a wiki,
though that has the advantage of an easy fallback case ("it's not markup,
so it must be plain text").
The task of the academic is not to scale great
intellectual mountains, but to flatten them.
> David Hopwood <david.nosp...@blueyonder.co.uk> writes:
>
>> To represent the closure of these shapes under intersection, you need
>> closed poly-arcs (i.e. closed shapes in which each segment is an arc,
>> which can be straight as a special case). And since circles and
>> rectangles are just examples of closed poly-arcs, you don't need to
>> represent them as separate classes or variants; that is just superfluous
>> complication.
>
> Complication but not necessarily superfluous. I'm certain that
> computing the intersection of two rectangles with sides parallel
> to axes is faster than doing it for generic polyarcs, so keeping
> specific shapes separate might be worth the effort. They need to
> be added, they don't have to replace the specific shapes.
>
If someone writes a good COLLIDE function (polyarch,polyarch) -> bool
in ML/Haskell (preferably ObjectLESS OCaml), I'll try to translate it into
an OO language of my choice.
We should then be able to experiment with how extensible, succinct and
robust these implementations are w.r.t extensions, specializations, etc.
> If someone writes a good COLLIDE function (polyarch,polyarch) -> bool
> in ML/Haskell (preferably ObjectLESS OCaml), I'll try to translate it into
> an OO language of my choice.
>
> We should then be able to experiment with how extensible, succinct and
> robust these implementations are w.r.t extensions, specializations, etc.
With CLOS-style generic functions the solution would be as follows:
- There is a generic function which converts any shape to a polyarch
(perhaps approximately)
- The implementation of intersection for generic shapes converts them
to polyarchs first.
- There are specialized implementations for chosen pairs of shape types.
That way adding and removing specialized implementations influence
only the quality and efficiency, not basic correctness.
It gets more fun when some shapes can't be exactly expressed as
polyarchs (e.g. Bezier curves) and there are multiple generic
representations of varying complexity and expressiveness.
>
> With CLOS-style generic functions the solution would be as follows:
>
> - There is a generic function which converts any shape to a polyarch
> (perhaps approximately)
>
> - The implementation of intersection for generic shapes converts them
> to polyarchs first.
>
> - There are specialized implementations for chosen pairs of shape types.
That was my plan, actually. If you want my job as the devil's advocate, you
can do the translation. We still need someone to write the ML version
first.
Forcing a default handler is the only solution I can think of in a
language that supports dynamic class loading. You then have the option
of throwing an exception in the default handler if you can't/won't
handle the sub type.
> Needless to say that this plays havoc with composability: adding two
> independent extensions to the system may break it. In other words,
> multiple dispatch breaks modularity (the ability to compose independent
> libraries without getting adverse interactions).
> For the Javaites among the readers: It also prevents dynamic class
> loading. Well, one could defer the error message until the
> not-provided-for type combination actually turns up and throw an
> exception. You'd get multiple dispatch at the price of slightly
> destabilising the software... which is actually feasible with the right
> overall architecture; see the whitepapers on system stability in Erlang.
In some cases having a default handler and special handling of a few
sub types is what the developer wants. In other cases you want special
handling of all sub types, but as you note this is impossible to check
at compile time on platforms that support dynamic class loading
(unless, like in the visitor pattern, the programmer is forced to
provide a list of supported sub types for multiple dispatch). Compiler
support for multiple dispatch could actually give you the option of
checking that all sub types available at compile time are handled in
separate methods, which would give some protection against the bugs
you mention.
/Jesper Nordenberg
Hmm... for some definition of "bilinear", yes ;-)
> Other structures no doubt exist - I'm having some fun exploring
> extensible parsing for a wiki, though that has the advantage of an
> easy fallback case ("it's not markup, so it must be plain text").
Which has its own problems - I'm involved in just such a wiki, too :-)
Re "other structures": I think one can often (maybe always) restructure
the problem, just as one can restructure the nonlinear Tree data
structure into linear "nodes" and "links". The transformation usually is
nonobvious (otherwise the software would have been structured
differently), and that's where inventiveness comes in.
One example is e.g. particle physics simulation. For N particle types,
one could write N*(N-1)/2 functions for defining the interaction between
them. Or one could write N functions that define the interaction of a
particle with the various fields :-)
Regards,
Jo
> Regarding Jon's ray tracer, I'll play the devil's advocate:
>
> When you only have spheres, algebraic types are nice and dandy, but if you
> had a plethora of different objects to render, some of them special cases
> of others, perhaps using OOP and organizing them into a class hierarchy
> would be better?
If you have N kinds of things ("objects") and M different things
("methods") you want to do with them, OO allows you to have the M
different "methods" close to each other for each "object", but the N
different "objects" will be spaced out. Using an ADT (ML/Haskell
datatype) allows you to keep the N "objects" together, but separates
the M "methods". You can view it as an NxM matrix that you lay out
either in row-major or column-major format.
Now, adding one "object" in OO allows the new code to be separated
from the old, but adding a new "method" requires global redefinition.
Using an ADT allows new "methods" to be added with separated code,
whereas adding a new kind of "object" requires global rewrites. In
both cases, the type system can help you figure out where you need to
add code for the difficult case. So it really boils down to whether
you need to add new "objects" more often than new "methods".
As for OO hierarchies, this really means that some "objects" may have
more "methods" than others. OO draws the hierarchy from the methods
perspective: Objects are classified by the methods they define. You
can also pick a object-centric view, where methods are classified by
which subset of objects they work on. Here, ADT's work fine: You can
have nested sum-types where a "method" is classified by which subtree
of the sum it works on. So, again, it depends on which axis you are
most likely to modify/extend.
As for which fits best in a ray-tracer, this depends on whether the
set of scene objects is more likely to be extended/subdivided than the
set of operations. I see no clear winner here, you can define it
either way: You can have a large number of different objects
(polygons, spheres, cylinders, etc.) and a single operation: Find
intersection with ray, or you can have a single type of objects (a
Bezier-patch) and a large number of operations: Find intersection,
find normal, find refraction index, find reflection index, find
bounding box, etc. OO may win in the first case, but it will
certainly lose in the second.
Even in compiler writing, where you are more likely to extend the
language on which you work instead of the number of things you do with
syntax, I personally find ADT's more to my taste than object
hierarchies: I like my type-checker to be separate from my parser and
code-generator, and things like optimising transformations (that has
to look at more than one node in the tree at a time) is a lot easier
using pattern-matching than classes.
Torben
The problem is: who't that "you"? Programmer A, programmer B, or the
system integrator C who wants to combine the extensions from A and B?
Neither A nor B know there's an interaction. C doesn't know about the
internals and cannot fix any problems that might arise (in fact he'll
probably never become aware of the problem - that's the worst of all
scenarios: subtle bugs that go unnoticed until somebody recalculates the
results by hand...)
> In some cases having a default handler and special handling of a few
> sub types is what the developer wants.
It's OK if the semantics is always the same but the implementation can
be optimised for common combinations. The worst that will happen is that
the (N+1)(M+1) case will take more time than needed - but since we're in
an opimisation scenario here, integrator C is most likely the person
doing the performance tests and looking for bottlenecks.
> In other cases you want special
> handling of all sub types, but as you note this is impossible to check
> at compile time on platforms that support dynamic class loading
> (unless, like in the visitor pattern, the programmer is forced to
> provide a list of supported sub types for multiple dispatch). Compiler
> support for multiple dispatch could actually give you the option of
> checking that all sub types available at compile time are handled in
> separate methods, which would give some protection against the bugs
> you mention.
Well, yes... but imagine a newbie programmer loading umpteen modules and
extensions (still in the phase of trying everything out) and getting
lots of errors that are related to modules he never even heard about!
Same can happen with experienced programmers: loading umpteen modules
and extensions because they are all needed, but first thing that happens
is that he'll have to write all those missing (N+1)(M+1) functions. Yet
another chore before you can actually begin with productive work *sigh*....
My personal favorite is to disallow multiple dispatch except within a
module boundary, where "module" is defined as "has one person or team
who's responsible for it" in addition to the usual modularisation
criteria. That way, programmers A and B are the same person and know
that they have a new (N+1)(M+1) case. (For example, this is how built-in
arithmetic is typically done: the author(s) of compiler and run-time
system know about all the admissible combinations, and if a new type is
introduced, they will be able to see all combinations that now need to
be handled. It's not a library context, but the issues are exactly those
we've been discussing - and if the arithmetic system is ever going to be
a programmer-providable module, this multiple dispatch problem needs to
be solved.)
Regards,
Jo
[ADT = algebraic datatype? Usually it stands for abstract datatype.]
> hierarchies: I like my type-checker to be separate from my parser and
> code-generator, and things like optimising transformations (that has
> to look at more than one node in the tree at a time) is a lot easier
> using pattern-matching than classes.
Indeed. In fact, I think your initial conjecture isn't even true: for
non-toy compilers it happens much more often that you introduce new
optimisations, new backends, whatever, i.e. new functions that operate
on (parts of) an AST, than that you change the language. Moreover, the
sheer number of auxiliary functions on ASTs that you need in a realistic
compiler already makes the OO approach unsuitable. Having details of
optimisation phases or multiple backends be scattered through a global
class hierarchy is totally the wrong kind of modularisation.
And I second pattern matching - you are likely to need nested patterns
for transformations, and those cannot be simulated by method dispatch
without introducing a maintenance nightmare.
Cheers,
- Andreas
--
Andreas Rossberg, ross...@ps.uni-sb.de
Let's get rid of those possible thingies! -- TB
> My personal favorite is to disallow multiple dispatch except within a
> module boundary, where "module" is defined as "has one person or team
> who's responsible for it" in addition to the usual modularisation
> criteria.
It's not sufficient for equality: you really want to define your own
equality for your own types, even in they come in different modules.
My personal favorite is no constraints, living with the possibility of
writing incomplete systems where certain combinations of types will
fail at runtime. This is because I know no system of constraints which
wouldn't throw out too many sensible programs.
My second favorite is Haskell classes with common extensions
(multiparameter classes, functional dependencies etc.). Availability
of operations is thus checked statically. The system is limited
by static typing, e.g. you can't use Haskell classes for different
kinds of nodes in an abstract syntax tree - you must use some other
mechanism for that. Algebraic types fit quite well, except that they
are not extensible.
> Joachim Durchholz <j...@durchholz.org> writes:
>
>>My personal favorite is to disallow multiple dispatch except within a
>>module boundary, where "module" is defined as "has one person or team
>>who's responsible for it" in addition to the usual modularisation
>>criteria.
>
> It's not sufficient for equality: you really want to define your own
> equality for your own types, even in they come in different modules.
Equality it too complicated anyway. And it isn't even decidable if
functions can be constructed at run-time (as is common in functional
languages).
Besides, as soon as the data structures get large, the concept of
equality becomes less and less clear.
Actually it isn't even clear for strings. If you have ASCII, multi-byte,
and UTF strings, should they be "equal" if they denote the same
character sequence, or should they be "equal" if they are byte-for-byte
equal? The answer is: it depends. Which means: the programmer should
select the appropriate equivalence relationship.
BTW this isn't limited to representational variation: sometimes the
appropriate equivalence relationship is case-insensitive comparison, and
nobody finds it strange that it's syntactically different from standard
string equality!
Next is equivalence unter mutability. Say, a data structure has some
expensive-to-compute field that's present if it ever was queried, absent
otherwise (FPLers will recognise this as a "memoised" value). Should two
data objects with a different status in this field be considered equal
or not equal? Answer (again): it depends. When (say) testing a
marshalling algorithm or doing other internal work, I want it included;
when looking at the data structure from the outside, I want it excluded.
(This is just a variant of representational equivalence.)
So my answer is: you don't need equality, you need equivalence. Which is
just another binary operator with a boolean result, and doesn't
introduce any problems that weren't present before.
Regards,
Jo
>> It's not sufficient for equality: you really want to define your own
>> equality for your own types, even in they come in different modules.
>
> Equality it too complicated anyway.
I encountered only two conceptual difficulties with equality:
- If many important types in a language are mutable (e.g. strings,
lists), the relation which should play the role of equality of these
values according to general principles (object identity) is usually
less often wanted than comparing elements.
- When comparing numbers, it's often useful to make models of the same
mathematical value expressed with different types compare as equal,
and to have special features of floating point equality (-0.0 is
equal to 0.0 even though they can be distinguished, NaN is not equal
to itself). This is incompatible with using equality for the most
distinguishing equivalence relation.
Of course various languages yield additional problems with associating
implementation of equality with types, but these are weaknesses of
particular languages, not conceptual problems.
> And it isn't even decidable if functions can be constructed at
> run-time (as is common in functional languages).
They should not be comparable for equality.
> Besides, as soon as the data structures get large, the concept of
> equality becomes less and less clear.
But for those types for which it is clear, the language should provide
a mechanism for this.
When I make a dictionary indexed by pairs of strings, I don't want to
be forced to manually specify how they should be compared.
> Actually it isn't even clear for strings. If you have ASCII,
> multi-byte, and UTF strings, should they be "equal" if they denote
> the same character sequence, or should they be "equal" if they are
> byte-for-byte equal?
They should be equal if they cannot be distinguished by other means,
not equal if they can.
Applying a lossy conversion between strings for determining equality
is asking for trouble. It's important which strings use which format.
Anyway, I see place for at most two string types in a general purpose
language: sequences of Unicode code points, and sequences of bytes.
> BTW this isn't limited to representational variation: sometimes the
> appropriate equivalence relationship is case-insensitive comparison,
> and nobody finds it strange that it's syntactically different from
> standard string equality!
Of course, but this is not equality, just some equivalence relation.
Structures like hash tables should not be restricted to using equality
on keys, as sometimes I want the keys to be compared e.g. case
insensitively. But equality should be the default because it's the
most common case and shouldn't be surprising.
My favorite way of providing the relation for key comparison is not
specifying a binary relation plus a hashing function, but specifying
a function which transforms the keys into things which should be then
compared using the default equality. Such function e.g. folds string
case, or extracts a tuple of fields from a record (if the equality
for this record isn't already defined to compare these fields).
This is not as general as an arbitrary binary relation in theory,
but covers all cases I encountered, and it's easier to use. No need
for providing a hashing function compatible with the equivalence.
It's also faster because the hash table stores transformed keys
instead of transforming them on the fly during searching.
> Next is equivalence unter mutability. Say, a data structure has some
> expensive-to-compute field that's present if it ever was queried,
> absent otherwise (FPLers will recognise this as a "memoised" value).
> Should two data objects with a different status in this field be
> considered equal or not equal?
Does code outside the implementation of that structure need to
distinguish between this field being already computed and not, or this
is an internal detail and the value is always computed when asked for?
In the first case the presence of the field should be taken into account,
in the second case it should not.
> Indeed. In fact, I think your initial conjecture isn't even true: for
> non-toy compilers it happens much more often that you introduce new
> optimisations, new backends, whatever, i.e. new functions that operate
> on (parts of) an AST, than that you change the language. Moreover, the
> sheer number of auxiliary functions on ASTs that you need in a
> realistic compiler already makes the OO approach unsuitable. Having
> details of optimisation phases or multiple backends be scattered
> through a global class hierarchy is totally the wrong kind of
> modularisation.
Have you ever tried writing a non-trivial compiler with the OO
approach? I have and maintain one on a daily basis. Now, I won't
dispute many of your claims, except that the OO approach is
unsuitable. You don't get too many auxillary functions scattered over
too classes unless your design is poor. Both the number of classes
and the number of functions in the compiler I maintain are too large
for me to track in my head (or even to estimate, other than to say
that both are well into the hundred(s)). However, I doubt that many
of the functions have specializations over more than a handful of
classes. The simple fact is that most functions if designed right are
not specialized except in terms of some very generic attributes, and
those attributes group into very regular hierarchies. Of course, the
hierarchies are not necessarily easily disentangled trees down to the
leaf level, so it is often useful to organize them as "properties"
(where the properties use inheritance hierarchies).
In addition, I would like to suggest that even in an evolving compiler
for a stable language, one is likely to add new AST types. That is a
very convenient way of adding a new specialization. If some
transformation yields a new result, it is useful to add one (or more)
AST types to represent the result of that transformation, even if
initially it may appear to be semanticly equivalent to some other AST
type. Adding the new type allows one to be more precise in the
semantics, as the transfromed node is often a special case of the
semanticly equivalent AST. For example, if you run a phase that sort
commutative operators so that constants only appear as the right
argument, it can be useful to add sorted_add AST nodes to indicate the
transformed outputs. Again, sorted may also be carried as a property
rather than a subtype, especially if it may be attached to many
distinct AST types. In either case, you can then specialize
transformations that only are safe (or are more efficient, etc.)
applied to sorted commuative operators.
I think OO inheritance gets criticized based on too many naive uses.
Too many people assume that it is a way of dividing the world into
distinct heirarchies, which doesn't match the shape of the world at
all. It is much better, for defining things where "this works like
that except for here and there". When used that way, one doesn't get
functions scattered over great masses of code. You get nice locality.
The point is when adding either N+1 or M+1, you usually have only a
few special cases to deal with, and you are generally adding both N+1
and M+1 at the same time, where the extra case(s) in the other
dimension is to capture some distinction your previous design didn't
distinguish, but you are really only adding a few cases to cover both
N+1 and M+1, 1 case for the primary body that applies almost
universally and some cases for the new distinguished elements,
depnding on how many categories of them there are.
Of course, this method only works if you are distnguishing things in
terms of characteristics (e.g. round, regular, parallel sides) rather
than final types (circle, ellipse, triangle, square, star).
-Chris
> Have you ever tried writing a non-trivial compiler with the OO
> approach? I have and maintain one on a daily basis. Now, I won't
> dispute many of your claims, except that the OO approach is
> unsuitable. You don't get too many auxillary functions scattered over
> too classes unless your design is poor.
I've written a compiler using generic functions
(http://sourceforge.net/cvs/?group_id=110425). This means that
dispatched functions are defined separately from types they dispatch
on, and their specializations are defined separately from generic
functions and from the types (I tend to write them near the
introductions of generic functions though).
There are several phases in the compiler. Each phase examines the
result of the previous phase and prepares data for the next phase.
Between most pairs of phases the module is represented using a family
of types for different kinds of tree nodes. Each time it's a different
family of types. Families of types usually form a two-level hiearchy,
for example "lifted" code has abstract supertypes for expressions,
patterns, definitions and meanings, and each of them is further split
into concrete types.
Below are the counts of generic functions dispatched on nodes of
each kind of intermediate code, for each module (I didn't count
other generic functions, e.g. dispatched on the types of literals).
A traditional OO approach would force to put all functions dispatched
on the same type together. For example pretty printing is quite
independent from other processing and it's used only for debugging
except the final phase, thus I defined it in separate modules, so it
doesn't get in the way when I'm looking at more important code.
(An OO approach would perhaps use fewer function names, because for
clarity I use different generic functions for domain subsets which are
never mixed and dispatched dynamically. For example I have separate
functions LiftExpr, Lift1Pat, Lift2Pat, Lift1Def, Lift2Def. An OO
approach would probably spell them just Lift, Lift1 and Lift2,
especially as they would be put in separate namespaces. So the real
function counts in an OO approach might be smaller, but the amount
of forced demodularization would remain the same.)
Source code:
- SourcePrinter: 1 (pretty printing)
- Expander: 17 (transforming source code to abstract code)
Abstract code:
- Abstract: 2 (type definitions, auxiliary functions)
- AbstractPrinter: 6 (pretty printing)
- Interfaces: 1 (generating interface files)
- Expander: 5 (some minor functions)
- Lifter: 17 (transforming abstract code to lifted code)
Lifted code:
- Lifted: 5 (type definitions, auxiliary functions)
- LiftedPrinter: 4 (pretty printing)
- Lifter: 3 (some minor functions)
- Planner: 16 (transforming lifted code to sequential code)
Sequential code:
- Sequential: 4 (type definitions, auxiliary functions)
- SequentialPrinter: 4 (pretty printing)
- Planner: 2 (some minor functions)
- CCoder: 12 (transforming sequential code to C code)
C code:
- CCode: 1 (type definitions, auxiliary functions)
- CCodePrinter: 8 (pretty printing)
- CCoder: 2 (some minor functions)
With a traditional OO approach the same design would be doable, at the
cost of forcing to put together type definitions of a phase, auxiliary
functions working on these types, pretty printing, and the main work
of transforming these values to the next representation. I prefer
to have freedom to arrange modules according to my taste rather than
being forced to put together functions just because they are dispatched
on the same family of types.
Currently the representations are independent from one another, and
a module which implements a transformation phase depends only on the
representation it examines and the representation it produces. The OO
approach would significantly increase the depth of the dependency
graph: each family of type definitions would depend on the following
phases (because it includes code which produces the next phase).
Technically this might not mean anything, it only hurts my taste.
In this language modules are used as units of name visibility. For
example each module which implements a transformation phase exports
only one name, a function transforming the whole module being
compiled. If visibility boundaries were tied to classes, I would have
to either make more names public, or introduce lots of "friendships"
(or whatever the language provides for extending visibility). I prefer
to be able to design visibility domains independently from grouping
functions for the purpose of dispatching.
> With a traditional OO approach the same design would be doable, at the
> cost of forcing to put together type definitions of a phase, auxiliary
> functions working on these types, pretty printing, and the main work
> of transforming these values to the next representation. I prefer
> to have freedom to arrange modules according to my taste rather than
> being forced to put together functions just because they are dispatched
> on the same family of types.
Two more points.
Besides generic functions which would correspond to dispatched methods
in OOP, there is other code. For example CCoder module (which
transforms sequential code to C code) includes many constants and
auxiliary functions for building parts of C code trees (something like
smart constructors, compositions of constructors etc.).
Note that I've even given this module the name which corresponds to
the kind of code it produces, not the kind of code it examines. This
transformation is more tied to the representation of its target than
to its source.
But with an OO approach these constants and functions operating
exclusively on C code would naturally go to the module which defines
types of C code, together with the pretty printer for the C code, but
far from the implementation of transformation of sequential code into
C code which actually uses them.
Well, I could write them in the module which uses them, i.e. the
module which includes the mentioned transformation (all of them would
be static constants or static functions), the module with sequential
code. This is consistent with my current design, but I'm afraid
contrary to typical OO practice which puts operations manipulating
values of some type together with the type definition, no matter
whether it builds them or examines them.
Another point. At some time I plan to add an internal interpreter (for
macros) of the language which is currently being compiled. This means
adding code which transforms some representation (probably lifted code)
into a representation which is suitable for execution.
In the current design this is possible by adding new modules, without
modifying any existing code. Of course they might trigger some tweaks
to existing code for convenience or efficiency, but in principle none
are needed.
In a design where functions dispatched on a type are defined together
with the type, I would have to add methods to existing classes.
The module which defines the lifted code would deal not only with
transforming it into sequential code for the compiler, but also with
making executable code for the interpreter from it.
I definitely prefer to have them written separately. The new module
will have little in common with existing code, it just dispatches on
the same family of types.
I admit that many of these issues are a matter of taste rather than
technical difficulties. It's hard to objectively examine how well code
is modularized and how focused are changes when it evolves.
> I've written a compiler using generic functions
> (http://sourceforge.net/cvs/?group_id=110425).
I think we have a different definition of what OO means, since much of
your description would match what I consider an OO approach, aside for
your persistent instance that certain things are defined separately.
And, I don't consider that to be relevant as to whether something is
OO. It's the dispatch to distinct functions which makes something OO,
because that's what allows the programming by differences. (That is
in my book, if one is doing dynamic dispatch (especially on type of a
function operand), one is doing OO. I realize that is not everyone's
definition, and for other people it is the strong association of
functions with type, which seems to be what your are critiquing.)
> Currently the representations are independent from one another, and
> a module which implements a transformation phase depends only on the
> representation it examines and the representation it produces. The OO
> approach would significantly increase the depth of the dependency
> graph: each family of type definitions would depend on the following
> phases (because it includes code which produces the next phase).
There is more likely to be some additional coupling in an OO approach.
However, not nearly as severe as you propose. It's more severe if you
use a "pure" OO approach where every function must be part of a type.
> I prefer to be able to design visibility domains independently from
> grouping functions for the purpose of dispatching.
That's not an unreasonable desire.
> This is consistent with my current design, but I'm afraid contrary
> to typical OO practice which puts operations manipulating values of
> some type together with the type definition, no matter whether it
> builds them or examines them.
Good OO practice puts the functions together which belong together.
The one caveat is that functions which are dispatched upon a type must
be associated with that type. However, good OO design would limit
that function to the part which is actually specialized.
> In a design where functions dispatched on a type are defined together
> with the type, I would have to add methods to existing classes.
Yes, it is true that the code which you wish to specialize on a type
must be defined "with" the type. Although in some OO languages, you
can "define" the type "piecemeal" and thus make this point moot. Even
in languages which require the type to be defined as one unit,
generally the code body for the function need not be in the type
declaration, so that one can still achieve the desired separation.
The type merely serves as a point where one can go to look and see a
listing of all the functions which specialize on it. The balance
between whether that is a burden or a blessing depends in part on
perspective.
-Chris
Um... that's true for languages where there's no life outside of classes
(e.g. Smalltalk or Eiffel). I always found that approach a bit too far
on the dogmatic side, and would want modules that are *not* classes. (In
fact I have seen and programmes a few classes that were just containers
for functions, not data abstractions. That's contrary to the True OO
Way, but it made the software definitely more manageable.)
Regards,
Jo
> My second favorite is Haskell classes with common extensions
> (multiparameter classes, functional dependencies etc.). Availability
> of operations is thus checked statically. The system is limited
> by static typing, e.g. you can't use Haskell classes for different
> kinds of nodes in an abstract syntax tree - you must use some other
> mechanism for that.
While type classes might not be suitable for individual nodes in a
syntax tree, they works well for syntactic categories -- expressions,
statements, declarations, etc. For example, you can make a "pretty
print" type class and instantiate it for each category, so you don't
need to invent names for each instance, and you can use a generic
(overloaded) function for such as error messages that return a message
string and a pretty-print of the offending sub-tree.
Torben
> alex goldman <he...@spamm.er> writes:
>
> > Regarding Jon's ray tracer, I'll play the devil's advocate:
> >
> > When you only have spheres, algebraic types are nice and dandy, but if you
> > had a plethora of different objects to render, some of them special cases
> > of others, perhaps using OOP and organizing them into a class hierarchy
> > would be better?
>
> If you have N kinds of things ("objects") and M different things
> ("methods") you want to do with them, OO allows you to have the M
> different "methods" close to each other for each "object", but the N
> different "objects" will be spaced out. Using an ADT (ML/Haskell
> datatype) allows you to keep the N "objects" together, but separates
> the M "methods". You can view it as an NxM matrix that you lay out
> either in row-major or column-major format.
Have anyone considered a language/environment that allows both kinds
of view of the matrix. I.e., you can switch between the views as you
like?
Consider a program written as guarded equations. In a functional
language, you wuld normally have all equations for a function right
after each other, one for each constructor in the datatype. But you
might also allow the equations to be spread out, so you collect all
equations for the same constructor, but different functions. A tool
could easily transform the program between the forms, or provide
different views of an internal representation that might be different
from both. Such a tool could also show which equations you would need
to add if you add a new constructor or function symbol (by making
"scaffolds" for each).
Such an approach would require equations to be independent of order,
i.e., either disjoint or by using a rule like in HOPE, where overlaps
are allowed if one pattern is more specific than the other (in which
case it takes precedence), so it wouldn't immediately work for, say,
Haskell or SML.
Torben
The obligatory reference to Rees's list of OO features or
properties:
<http://store.yahoo.com/paulgraham/reesoo.html>
--
Jens Axel Søgaard
> The obligatory reference to Rees's list of OO features or
> properties:
>
> <http://store.yahoo.com/paulgraham/reesoo.html>
For my language the only strong "no" is 9. Others are either "yes"
(1, 3, 4, 5, 7), or "can be made true but the common design makes it
false" (2), or "technically true but probably not in the way it was
intended" (6), or "hard to tell whether a feature qualifies as this"
(8). The concepts are too vague for me to be able to clearly state
whether they are supported.
I understood the Andreas's critique as criticizing 9.
Argh... that list is a horrid mixture of valid points, non sequiturs,
and simple ideology. If an item is in the list, this says more about the
mental state of those who used it than about whether a language is OO or
not...
Judging by the commentary that follows the list, Jonathan Ree was well
aware of that. Just please don't use this list to decide whether your
favorite language is OO! I think points #1 (encapsulation), #3 (ad-hoc
polymorphism), #5 (everything is an object), #7 (subtyping/specification
inheritance), #8 (implementation inheritance), and #9 (dispatch on first
parameter) can indeed be used to characterise a language as "more" or
"less" or "this kind of" OO. Say, a language can be considered OO if it
has at least 50% of these points, or something like that...
#2 is just a consequence of #1 done well. Together with #4, these are
non sequiturs: there are distinctly non-OO languages that have it
(particularly the first public version of Ada), and there are OO
languages that don't have it (Smalltalk, early Eiffel). So these
criteria are useless to decide whether a language is OO or not.
#6 is just Smalltalk ideology. If you look at the way that "message
sends" are done or defined, you find it's dynamic dispatch (already
covered as #9) as the only access method (#1) plus a standard subroutine
call (another non sequitur).
Regards,
Jo
> Chris F Clark wrote:
>> (That is in my book, if one is doing dynamic dispatch (especially
>> on type of a function operand), one is doing OO. I realize that is
>> not everyone's definition, and for other people it is the strong
>> association of functions with type, which seems to be what your are
>> critiquing.)
I think it is an overly broad definition, that is, if I understand the
terms correctly. Isn't algebraic data types a form of dynamic
dispatch? Certainly Haskell's classes seem to count.
Is Haskell then an OO language?
> The obligatory reference to Rees's list of OO features or
> properties:
>
> <http://store.yahoo.com/paulgraham/reesoo.html>
I don't think this list is particularly good, either. E.g. in point
8, the description of "implementation inheritance" is so vague as to
cover almost any kind of abstraction. Perhaps that's a slight
exaggeration on my part, but I don't see why it wouldn't cover
e.g. HOFs.
-k
--
If I haven't seen further, it is by standing in the footprints of giants
Nice list (although I don't really see the difference between
encapsulation and protection).
> I understood the Andreas's critique as criticizing 9.
Right. IMO the defining characteristic of "object-oriented" programming
is structuring programs around autarkic "objects" that contact each
other in some way, but which all decide "by themselves" how they react
to that, in a black-box sort of manner. That's what the term suggests,
and it is the only characteristicum from the above list which you do
/not/ find in other paradigms one way or the other. I sometimes call it
"internalisation of operations", wrt a value. Technically, this is
usually modeled by the "sum-of-product-of-function" idea described in
the list.
I'm aware that some people are using the term OO in a wider sense, but I
don't like it. First - as Rees notes correctly - it practically renders
the term meaningless. Second, I believe it usually stems from either the
desire to comply with the marketing hype ("see, <my favorite language>
is OO too"), or the attempt to argue for the superiority of OO ("see,
all modern languages are OO"), or from the lack of background regarding
many of the ideas now captured as "inherently" OO ("OO is great because
it gives us modularity"). I find neither an acceptable reason.
In particular, I disagree with considering "dynamic dispatch" or "late
binding" OO. Or encapsulation. Or polymorphism. All are ingredients for
realising the above idea, but not defining characteristics. All of them
are found in any decent language. Often in much more flexible ways than
in typical OO languages.
>>The obligatory reference to Rees's list of OO features or
>>properties:
>>
>> <http://store.yahoo.com/paulgraham/reesoo.html>
>
> I don't think this list is particularly good, either.
If nothing else the lists shows that one mans OO is
different from another's. As such it is provides
a good vehicle to get an OO discussion on track.
That is, I think Chris's and Marcin's discussion
is more interesting than the list it self.
--
Jens Axel Søgaard
> Right. IMO the defining characteristic of "object-oriented"
> programming is structuring programs around autarkic "objects" that
> contact each other in some way, but which all decide "by themselves"
> how they react to that, in a black-box sort of manner. That's what the
> term suggests, and it is the only characteristicum from the above list
> which you do /not/ find in other paradigms one way or the other.
FWIW, I've also heard this classified as "object based", while "object
oriented" is taken to include inheritance as well. Seems like a
sensible distinction to me.
> I think we have a different definition of what OO means, since much of
> your description would match what I consider an OO approach, aside for
> your persistent instance that certain things are defined separately.
I don't know what should be considered OO.
I do have an opinion about technical characterization of functional
programming:
1. working with immutable values
2. functions are values, including local functions
but not so for OOP. I don't even know whether my language would
qualify as OO.
> And, I don't consider that to be relevant as to whether something is OO.
Of course it's not relevant for judging how much we like it, but it
would be nice to have a common terminology to avoid confusion.
I just googled for object-oriented, went to the 10th page (because
first pages seem to be crowded with generic introductions instead of
more real-life usages of the term), and looked at some links:
"The first one was about the report I am writing about the
possibilities of using object-oriented programming in game design."
"Oolex (object-oriented lexer) presents a new idea for lexical
analysis: the scanner is strictly based on the object orientation
paradigm."
I would like to know whether authors mean objects which contain named
functions (the 9th item in Rees's list), which is similar to sending
messages (5), or something else - maybe just relying on dynamic
dispatch.
> It's the dispatch to distinct functions which makes something OO,
Perhaps there should be more specific terms to avoid confusion.
I tend to say "traditional OOP" to mean that functions are embedded in
objects, or that an object has a fixed set of messages it responds to,
but I'm afraid it's not sufficiently unambiguous. "Class based OOP"
is too narrow for this if we want to include prototype based OOP.
"Receiver-based OOP"?.
For the wider sense perhaps "relying on dynamic dispatch" would be
enough.
Yes, "object-based" was used more frequently in the early nineties(?)
when prototype-based languages were more en vogue, but I haven't heard
it for quite a while. I rather recall the distinction being between
absence and presence of classes. Of course, the latter usually includes
inheritance (I wouldn't try to classify anything via inheritance alone,
because of the even wider confusion about what inheritance is to start
with).
Yes, this is the direction I am headed (albeit in some limited ways).
For example, in my parser generator, I am working on implementing the
visitor pattern. Now, it is useful to be able to group the visitor
pattern either by AST node or by visiting function. The result is
exactly a matrix. However, my current plans were not to implement it
as such--this exchange is prompting me to reconsider that.
I could see doing so based on another tool that I am working on which
is based on the decision table model. In decision tables, you have a
matrix with columns that represent different choices (things you wish
to consider in your dispatch) and rows that represent actions, things
you wish to happen when the dispatch occurs. The entries in the
matrix represent whether the action happens or not. (It is possible
to use an inheritance model to build the matricies out of simpler
versions.)
You can also use a slightly different representation, similar to
Karnaugh maps, if the dispatch involves a series of questions. The
rows and columns of the matrix are the answers to the questions, and
then entries in the matrix represent specific combinations of the
answers, within the entry are the action (or list of actions) for that
combination.
It is easy enough to switch from one view of the matrix to another,
both are just projections from n-space down to 2-space. Similarly,
one can build the matrix out of "declarations" that [partially] fill
in one or more entries--that's a projection down to 1-space.
If the tool is done right, one could write the declarations
distributed in the manner that was convenient to the developer, and
view the matrix in a variety of formats so that one could find errors
easily.
-Chris
Actually I think that prototype-based languages are "sufficiently
different" to say they are "closely related to OO because they share
many characteristics, but they are a distinct breed despite".
IOW no, I don't think they are OO in the usual sense. Maybe that's
because I never have done serious programming in them ;-)
Regards,
Jo
I think the point made on the page is valid:
> Perhaps part of the confusion - and you say this in a different way
> in your little memo - is that the C/C++ folks see OO as a liberation
> from a world that has nothing resembling a first-class functions,
> while Lisp folks see OO as a prison since it limits their use of
> functions/objects to the style of (9.).
I come from the liberation school, which is my view of what is OO is
wider. Learning OO allowed me whole new ways of modelling things. It
did not render what I had previously learned, e.g. structured
programming invalid. The definition of OO, I learned, was 5 points:
encapsulation, polymorphism, inheritance (and if I recall correctly
abstraction, and dynamic dispatch). Encapsulation and abstraction are
simply "modular programming" and not new to OO. That means for me, OO
was about polymorphism, inheritance, and dynamic dispatch, as those
are the things which "we did not know" prior to OO.
So, yes, I would consider pattern matching a form of dynamic dispatch
and thus something OO, albeit a newer and more interesting form than
OO itself (prior to FP languages) provided.
In fact, I don't consider HOF to be an FP innovation (at least not a
modern one), since HOF have been known and desirable for as long as I
can remember. In fact, one of the frustrations with Pascal (a pre-OO
language) was in trying to write HOFs--the argument passing powers
were more a hinderance than a help, which actually helped make C
popular, since it could do HOFs better.
The distinguishing characteristic of modern FP is type inference, a
better way of writing generic functions. This makes it possible to
write HOFs in a type-safe way. That is an innovation. That
distinguishes it from OO. No amount of inheritance based polymorphism
is going to give one the same freedom as type inference.
Modern FP also inherits from vernerable FP (e.g. Lisp) the idea of
applicative programming, i.e. expressions without side-effects. It
also inehrits the idea of returning "large values" (e.g. lists) rather
than only things which fit in a word. However, I would argue that
both of those have been seen as desirable (with the side-effect-free
part perhaps questionably so) for a long time, even by the non-FP
programmers in the Pascal era.
So, if you want to argue that type inference enaables solutions that
can't be expressed by inheritance, get on your soap-box and we will
listen, especially when you have evidence. Similarly, if you can
claim advantages from immutability, let us know. We OO people want to
learn and grow. (Personally, I am most interested in monads, to see
if they are really a boon. Generally, you see GC adopted by OO and
tail-call-optimization will follow suit.) However, if you want to
criticize us for some slavish adherence to some rule, be careful, we
don't consider ourselves slaves. When I consider myself to be a
convert on FP (and I'm tettering as I find certain aspects of mutable
values really convenient), that doesn't mean I will stop considering
myself an OO programmer (nor even a structured programmer). Knowledge
grows by acretion. You don't need to forget what you've learned in
order to learn more (Well, sometimes you do for a short while, but not
universally and not eternally).
And, yes there are purists, people who are slavish devoted to solving
everything one-way. However, much of that purity is motivated by
practical considerations. For example, in a langue where everything
is an object, what one is really asking for is for everything to be
first-class. Something I think most FPers will appreciate. It's a
real pain when "integers" and "objects" have different rules and
one-or-the-other is not first-class.
So, please add closures and pattern matching to the next language I
use. If you give me bignums, I won't complain either. Just don't
take away inheritance if you can avoid it. It really does solve
certain problems in an elegant way that does minimize code. And the
penalty for using it isn't very high if one finds the right hierarchy.
-Chris
My rule of thumb is that if it can handle the shapes example, then it's
an object oriented programming language. About as objective as any
other definition of what OOP means. The downside is that this puts
BrainF*ck into the OO PL camp. :-)
http://www.angelfire.com/tx4/cus/shapes/index.html
(Still mulling how to best do the example for Alice ML).
Chris Rathman.
> My rule of thumb is that if it can handle the shapes example, then it's
> an object oriented programming language. About as objective as any
> other definition of what OOP means. The downside is that this puts
> BrainF*ck into the OO PL camp. :-)
>
> http://www.angelfire.com/tx4/cus/shapes/index.html
Why does C# define accessor methods instead of making fields public?
The simplest solution in my language Kogut doesn't use anything
resembling inheritance, and is quite short compared to other entries:
// Shapes
let MoveTo shape newX newY {
shape.x = newX;
shape.y = newY
};
let RMoveTo shape deltaX deltaY {
shape->MoveTo (shape.x + deltaX) (shape.y + deltaY)
};
let Draw (shape!) {};
// Rectangles
type RECTANGLE = Rectangle (var x) (var y) (var width) (var height);
let Draw (rect ! RECTANGLE) {
WriteLine "Drawing a rectangle at:(" rect.x "," rect.y "), \
Width " rect.width ", Height " rect.height
};
// Circles
type CIRCLE = Circle (var x) (var y) (var radius);
let Draw (circ ! CIRCLE) {
WriteLine "Drawing a circle at:(" circ.x "," circ.y "), \
Radius " circ.radius
};
// Try shapes
let scribble = [(Rectangle 10 20 5 6) (Circle 15 25 8)];
Each scribble ?shape {
shape->Draw;
shape->RMoveTo 100 100;
shape->Draw
};
let rect = Rectangle 0 0 15 15;
rect.width = 30;
rect->Draw;
Many of the languages could have been shortened by either directly
accessing the fields or by using automatic accessors (like ruby).
Can't say that I was always consistent, but the thing I was trying to
do was show how the methods and properties interact. For many of the
languages, this meant that the specific example could have been
shorter. It's the most common complaint I get for the various
languages.
Anyhow, the manual accessors help make the example read better when
hopping from language to language, which is more what I was interested
in rather than showing off a particular language's expressiveness.
> The simplest solution in my language Kogut doesn't use anything
> resembling inheritance, and is quite short compared to other entries:
Thanks for the entry. Not sure I understand how the method dispatch
occurs within Kogut.
Chris Rathman
Ironically, FPLs predate OO by a decade IIRC.
(Lisp: sometime in the 50ies, Simula in the 60ies - I'm too lazy to
check the exact dates, but I'm pretty sure somebody knows them by heart *g*)
> In fact, I don't consider HOF to be an FP innovation (at least not a
> modern one), since HOF have been known and desirable for as long as I
> can remember. In fact, one of the frustrations with Pascal (a pre-OO
> language) was in trying to write HOFs--the argument passing powers
> were more a hinderance than a help, which actually helped make C
> popular, since it could do HOFs better.
The essential point is that you can construct functions at run-time.
This is not possible in Pascal or C, and marginally possible in C++.
> The distinguishing characteristic of modern FP is type inference, a
> better way of writing generic functions. This makes it possible to
> write HOFs in a type-safe way.
HOFs could always be type-safe. The point of having type inference is
that it makes writing type-safe functions easier, because the type
signatures of a HOF can be quite mind-boggling. E.g. in a Pascal-style
syntax, the fold function would look like this:
function fold (function fun (A): B; initial: A; l: list of A): B
Now fold has a very simple signature; imagine a third-order function
that takes a function taking functions as parameters...
No, type inference doesn't make HOFs possible, but it does a large step
towards making them practical. (Parametric polymorphism is the other
important feature.)
> Modern FP also inherits from vernerable FP (e.g. Lisp) the idea of
> applicative programming, i.e. expressions without side-effects.
Ironically, applicativity was one of the first things that Lisp lost -
with the introduction of RPLACA and friends.
Though the applicative style never got completely lost. Both styles seem
to have coexisted through the years.
> So, if you want to argue that type inference enaables solutions that
> can't be expressed by inheritance, get on your soap-box and we will
> listen, especially when you have evidence.
Soapboxed FPLers typically talk about parametric polymorphism (not type
inference) being the FP equivalent to ad-hoc polymorphism.
Actually most will readily acknowledge that OO-style polymorphism is
slightly more powerful, but they will also say that parametric
polymorphism avoids a whole slew of problems that are typical for
working with OO class hierarchies: dispatching asymmetries (rare but
requires premature design decisions if it occurs), dissemination of
logic across a class hierarchy (a common problem), no good way to
modularise a class from its subclasses (roughly: private is too
restrictive, protected/public exposes too many implementation details).
I have not enough experience with FPLs to say for sure that the argument
holds up in practice, but I have done enough OO class hierarchy design
to find the arguments plausible.
> Similarly, if you can claim advantages from immutability, let us know.
Oh, that's really a FAQ, but anyway, here goes:
All problems that can be associated to pointer aliasing go away.
Instantly and without a trace.
There is no need to copy data structures. Since data will never change
when you're not watching, you can "copy" it by creating a new pointer to it.
In practice, FPLs with no mutable data structures don't even have the
concept of a "reference" - all assignment is by reference, since that's
a safe thing to do under immutability, so there's no need to have
by-value copying.
The absence of aliasing problems allow compilers to do a lot of
aggressive high-level optimisations. Every function can be recast as a
macro expansion and vice versa (try that with MIN() in C...), with no
worries about breaking things. (The worries for an FPL compiler writer
are the heuristics under which a function should be inlined...)
> (Personally, I am most interested in monads, to see if they are
> really a boon.
The topic is vastly overrated. They are just a specific Design Pattern.
Since this particular pattern is extremely abstract, many people waste
far too much time trying to understand the concept. Unfortunately,
nobody with enough knowledge and talent (and interest) has written a
good generally understandable explanation.
(The best approximation that I have come up with is "a monad is what's a
pipe in Unix, only type-safe", but (a) I'm not sure that this is fully
correct and (b) I'm pretty sure that this still isn't the full picture.)
> Generally, you see GC adopted by OO and
> tail-call-optimization will follow suit.) However, if you want to
> criticize us for some slavish adherence to some rule, be careful, we
> don't consider ourselves slaves. When I consider myself to be a
> convert on FP (and I'm tettering as I find certain aspects of mutable
> values really convenient), that doesn't mean I will stop considering
> myself an OO programmer (nor even a structured programmer).
I had a somewhat sobering experience, and I don't consider myself an OO
programmer anymore.
The experience was this: I was part of the basic library standardisation
efforts for Eiffel. We tried to capture all relevant aspects of the
STRING class' in the form of preconditions and postconditions. I learned
two lessons there:
1) Doing assertions over containers without higher-order functions, by
direct recursion, is a nightmare and not really worth the effort. (We
had several cases where a function's body was easier to read than the
assertion. The problem would have gone away if we had been able to
directly write things like "forall characters in Current, this-and-that
holds".)
2) One of the dreams in the Eiffel community were that, given a set of
"precise enough" postconditions, the Eiffel compiler would be able to
generate the routine's body. It came as a shock to me to see that such a
postcondition would already be the function's body in an FPL - so why
wait until Eiffel acquires an "implementation inferencer" (which would
be *very* difficult to write since Eiffel doesn't have a formal
semantics and other problems that would make is very difficult to have
anything reliable for that).
3) Mutability makes it entirely impossible to nail down some essential
aspects of the workings of such a class. For example, if you write an
Eiffel loop like
loop
...
"foo".append(something)
...
end
you'll find that the STRING object will be reused, and grow with every
iteration. This isn't what one would expect, but it's perfectly in line
with mutable string semantics: "foo" is an instruction to the compiler
to create a (mutable) STRING object, and the language designer didn't
write it into the specs whether literal strings have to be recreated on
every loop iteration. (He even refused to add such a notice when asked
about the problem, because having to create the string object on every
iteration would be "inefficient".)
Well, of course I still can program in OO languages. It's just that I
don't see much value in inheritance anymore (but I gladly accept the
encapsulation that OO classes bring).
> And, yes there are purists, people who are slavish devoted to solving
> everything one-way. However, much of that purity is motivated by
> practical considerations. For example, in a langue where everything
> is an object, what one is really asking for is for everything to be
> first-class. Something I think most FPers will appreciate. It's a
> real pain when "integers" and "objects" have different rules and
> one-or-the-other is not first-class.
Indeed. Though I'm not sure how that translates to FPL usage - I haven't
noticed anything "not quite functional" about FPLs.
> So, please add closures and pattern matching to the next language I
> use. If you give me bignums, I won't complain either. Just don't
> take away inheritance if you can avoid it.
Inheritance makes type inference largely impossible.
If you have a call that says "foo.frobnicate", you don't know which of
the many classes in your system contains the "frobnicate" function
that's meant.
Well, if all "frobnicates" are in descendants of the same base class,
you can indeed do type inference - but what if they are in different
class hierarchies? From reading the program, neither the programmer nor
the compiler could infer what semantics is actually meant.
> It really does solve certain problems in an elegant way that does
> minimize code. And the penalty for using it isn't very high if one
> finds the right hierarchy.
The same can be said for parametric polymorphism :-)
The real question is: it parametric polymorphism enough? And if it isn't
and you have to circumvent the type system: are its other advantages
worth the restrictions?
However, I have one indirect evidence that it's indeed enough: All OO
languages that I know have some way to access RTTI, and to circumvent
the type system to work around the expressivity limitations of the type
system. Type casts (checked in better languages) are quite commonplace
and routinely applied by application programmers.
No such mechanism is available in a typical FPL. Well, some FPLs do have
it, but the docs lace such functions with such heavy warnings that
application programmers almost never use them. And it's very rare that a
programmer calls for such a thing here in comp.lang.functional.
To me, this indicates that parametric polymorphism is both powerful
enough and leads to less typing problems.
Regards,
Jo
Yes, but I think it results in very deep changes to a language, and
doing it right would be much harder than might first seem apparent.
So, modularity is the reason we can write nontrivial programs. We can
make new components out of old ones, and then think about the new
components as black boxes. All medium-sized and bigger programs do
this, with varying degrees of support from the language. Modularity
gets formalized with a contextual equivalence theorem; a a pair of
code chunks are equivalent implementations of a module if you can
substitute them into any client program and get equivalent behavior.
Now, when you can "swap" between the views, then what's client, and
what's module *changes* when you swap things around. So the question
of how to do modular programming in such an environment has a lot of
question marks next to it.
The aspect-oriented programming community faces many of the same
issues; aspects can invasively change the implementation of a module,
and the implementor doesn't necessarily know what aspects will be
applied to the module. See
<http://aosd.net/pipermail/discuss_aosd.net/2005-April/001527.html>
for more discussion of this point.
--
Neel Krishnaswami
ne...@cs.cmu.edu
The following is describes something similar :-
@inproceedings
{ Ossher:iccl:1990
, author= "Harold Ossher"
, title= "Multi-Dimensional Organization and Browsing of
Object-Oriented Systems"
, crossref= "iccl:1990"
, pages= "128--135"
, refs= 10
, source= "copy"
, checked= 20001007
, abstract= "This paper describes a two-dimensional organization for
object-oriented systems, and a browser supporting that
organization. The organization provides sites for documenting both
generic functions and object types, allows convenient browsing and
information hiding according to both function and type, and supports
the notion of {\em abstract types}. The paper then describes
extension of the organization and browswer to multiple dimensions
to allow for {\em multi-methods} that are split into separate
implemetnations based on criteria in addition to receiver type.
Inheritance and information hiding in the multi-demensional case are
discussed briefly. The multi-dimensional browser had been
implemented on top of the RPDE environment framework."
}
@proceeedings
{ iccl:1990
, booktitle= "International Conference on Computer Languages"
, year= 1990
}
Indeed.
> Actually most will readily acknowledge that OO-style polymorphism is
> slightly more powerful
No, it's not. The two are incomparable in expressiveness. But as you
say, parametric polymorphism is simpler and less problematic. And more
extensible.
> 3) Mutability makes it entirely impossible to nail down some essential
> aspects of the workings of such a class. For example, if you write an
> Eiffel loop like
> loop
> ...
> "foo".append(something)
> ...
> end
> you'll find that the STRING object will be reused, and grow with every
> iteration. This isn't what one would expect, but it's perfectly in line
> with mutable string semantics: "foo" is an instruction to the compiler
> to create a (mutable) STRING object, and the language designer didn't
> write it into the specs whether literal strings have to be recreated on
> every loop iteration. (He even refused to add such a notice when asked
> about the problem, because having to create the string object on every
> iteration would be "inefficient".)
Incidentally, OCaml with its, um, debatable feature of mutable strings
has exactly the same problem. :(
> Inheritance makes type inference largely impossible.
No, inheritance is not a problem. Subtyping is. And it does not strictly
make type inference impossible, only much less practical. You usually
have to give up completeness, i.e. resort to local type inference.
Cheers,
- Andreas
Yes, and doesn't the size of this table hence make precisely my point,
i.e. that such a broad definition of OO is utterly useless? ;-)
> (Still mulling how to best do the example for Alice ML).
I don't hesitate at all to concede that Alice ML is not an
object-oriented language. :-)
Having said that, as the page's problem description stands, it is
straightforward to solve with ML modules:
signature SHAPE =
sig
val draw : unit -> unit
val moveTo : int * int -> unit
val rMoveTo : int * int -> unit
end
functor Shape (val x:int val y:int) =
struct
val x = ref x and y = ref y
fun moveTo (x', y') = (x := x'; y := y')
fun rMoveTo (dx, dy) = (x := !x+dx; y := !y+dy)
end
functor Circle (val x:int val y:int val r:int) =
struct
structure Shape = Shape (val x=x val y=y)
open Shape
val r = ref r
fun setRadius r' = r := r'
fun draw () = ...
end
functor Rectangle (val x:int val y:int val w:int val h:int) =
struct
structure Shape = Shape (val x=x val y=y)
open Shape
val ref w and h = ref h
fun setWidth w' = w := w'
fun setHeight h' = h := h'
fun draw () = ...
end
functor DoSomething (S : SHAPE) =
struct
val _ = (S.draw (); S.moveTo (100, 100); S.draw ())
end
structure R = Rectangle (val x=10 val y=20 val w=5 val h=6)
structure C = Circle (val x=15 val y=25 val r=8)
structure _ = DoSomething R
structure _ = DoSomething C
Note that the problem description does not explicitly require shapes to
be first-class ;-). Also note that I even mimicked inheritance.
Now, if you really need shapes first-class, then you need first-class
modules. In Alice ML you can abuse packages for this, but you loose some
static typing (with static first-class modules like in Moscow ML you
wouldn't):
signature RECTANGLE =
sig
include SHAPE
val setWidth : int -> unit
val setHeight : int -> unit
end
signature CIRCLE =
sig
include SHAPE
val setRadius : int -> unit
end
val r = pack R : RECTANGLE
val c = pack C : CIRCLE
fun doSomething p =
let
structure S = unpack p : SHAPE
in
S.draw (); S.moveTo (100, 100); S.draw ()
end
val _ = List.map doSomething [r, c]
Does that suit you?
Caveat: all code untested.
- Andreas
I rather had first-class functions in mind, and those were known long
before OO. Or if you want something more recent and fancy, Haskell-style
type classes.
IMO, what was really novel in OO at the time was the structured way in
which inheritance and polymorphism were brought in, along with a
programming paradigm embracing them.
> In fact, I don't consider HOF to be an FP innovation (at least not a
> modern one), since HOF have been known and desirable for as long as I
> can remember. In fact, one of the frustrations with Pascal (a pre-OO
> language) was in trying to write HOFs--the argument passing powers
> were more a hinderance than a help, which actually helped make C
> popular, since it could do HOFs better.
Real HOFs means closures, and Pascal did not have them. Closures require
garbage collection and hence had a pretty hard time in the mainstream
for decades. Both technologies were pioneered in FP.
> The distinguishing characteristic of modern FP is type inference, a
> better way of writing generic functions.
Schemers definitely would disagree very strongly with putting their
language in the old-fashioned corner! ;-)
> Modern FP also inherits from vernerable FP (e.g. Lisp) the idea of
> applicative programming, i.e. expressions without side-effects.
Yes, still one of the central themes.
> It
> also inehrits the idea of returning "large values" (e.g. lists) rather
> than only things which fit in a word. However, I would argue that
> both of those have been seen as desirable (with the side-effect-free
> part perhaps questionably so) for a long time, even by the non-FP
> programmers in the Pascal era.
Sure, these ideas (including HOFs) already occurred during the
development of Algol and Algol-68, but time wasn't ripe. (But some of
the involved people later became pioneers for FP.)
> So, if you want to argue that type inference enaables solutions that
> can't be expressed by inheritance, get on your soap-box and we will
> listen, especially when you have evidence.
Type inference is really really neat and spares us a lot of tedium. I
wouldn't identify it as a crucial part of FP, though.
> Similarly, if you can
> claim advantages from immutability, let us know.
Well, I'm not a purist, so I wouldn't argue for banning mutability
altogether. But neither does any FPL, if you look closer. The puristic
ones just chose to reflect and control it in types, e.g. by means of
monads or by linear typing. I have some sympathy for this idea, but as
far as I'm concerned the jury is still out.
On the other hand, the advantages of at least minimizing and localising
state are obvious, and I see a general tendency in that direction not
only in the FP world. For example, I often have to deal with
concurrency, and in its presence mutability is very very hard and
intricate to control and hence has high potential for obscure bugs. Even
worse with distributed programming. It's no coincidence that many
protocols in Internet technology tend to be (almost) stateless. I
believe that general programming will move further in that direction the
more ubiquitous concurrency and distribution become.
> For example, in a langue where everything
> is an object, what one is really asking for is for everything to be
> first-class. Something I think most FPers will appreciate.
Yes, I fully agree. I found that point on the list a bit odd myself.
Cheers,
- Andreas
That's kind of the way I've been leaning to get the polymorphism to
work with the dynamic dispatch. Now, if you could do such without have
to pack and unpack them, you'd have an OO language. :-)
I'm also thinking that the casting can go both ways, giving Marcin his
downcasting capabilities that he misses in O'Caml.
Chris Rathman.
I see the smiley, but anyway, isn't that merely a syntactic difference?
Where do you draw the line? For example, with OCaml's objects you need
an explicit coercion for this example, which is similar to a pack. You
do not need to unpack, though. Is that the line? What if packages were
statically typed, like in Moscow ML? Then you could consider allowing to
omit the signature annotation at unpack. The line would be even thinner.
Finally you could define some syntactic sugar like:
p.#a == let structure X = unpack p in X.a end
Now the line has vanished, solely by adding syntactic convenience.
> I'm also thinking that the casting can go both ways
You mean in my example? Yes, you always can recover the most specific
signature with unpack. That is indeed like a sort of downcast.
Agreed. Been typing faster than thinking...
(actually I meant to say "interface inheritance", which is a kind of
subtyping)
> And it does not strictly make type inference impossible, only much
> less practical. You usually have to give up completeness, i.e. resort
> to local type inference.
Is there a short text that outlines the nature of these problems? I find
that my mental model of what subtyping does to type inference is
seriously lacking.
Regards,
Jo
Nothing introductory I'm aware of at the moment. But the basic problem
is simple: without subtyping, type inference just amounts to deriving
and solving a system of equations over types. With subtyping, this
becomes a system of inequations, which makes it explode combinatorially,
because you cannot perform much simplification anymore.
A good example of this is my referral to parametric polymorphism as
type inference. I should understand that difference, but I have yet
to do enough FP programming myself to cement that in my mind. (I
don't count the small amount of programming I do in lisp as FP,
because I don't really use map or lambda (except as part of defun),
although I don't use replaca either (just cons/append), so I believe
my code is "pure".)
Hopefully, no one took what I was saying as a criticism of FP, in my
process of trying to defend what I thought was good about OO.
> IMO, what was really novel in OO at the time was the structured way in
> which inheritance and polymorphism were brought in, along with a
> programming paradigm embracing them.
Yes, and I could see a criticism of people who get too slavishly
immersed in that OO structuring as a solution to all problems. I must
admit I am close to having that fault. Not working in a language with
closures and a fold function, I tend to structure my solutions
differently, so that the advantages of a fold function are less
apparent. Therefore, it is easy for me to say, I have this nice
solution that works for me, but not see that there might be other
solutions that might be even nicer, if I shifted perspective.
But this is close to my point. There are OO solutions and they are
workable, more workable than I think credit is given to them. Sure,
there may be solutions using parametric polymorphism that are more
elegant in some dimension. However, there are also points of elegance
that are lost by abandoning OO. There are points to be learned by
trying solutions in different spaces. The different perspectives
allow one to see all the different ways of making the code elegant.
I believe there are quotes by both Dijkstra and Knuth extolling the
virtues of at least occassionally programming something in a much more
restrictive environment or paradigm, as it tends to open ones eyes
better. I can remember the one from Deremer, "Jail is liberating."
(There were times that my PL/I programming becamse better, because I
learned COBOL and how it solved certain problems.) In that light,
think there is much to be learned by figuring out how to structure an
OO program elegantly. It forces one to understand certain aspects of
the problem very well and to really think through certain issues. I
think the understanding of those issues is lost if one goes to a
parametric polymorphic solution, because one doesn't have to resolve
them (and they are then not resovled in an elegant fashion).
I think Jo's example of trying to write invariants et. al. for the
Eiffel runtime are most illustrative. If he hadn't tried to solve that
problem (and most of us never face that problem, because we don't write
in a language with contracts), he wouldn't have seen the elegance of
FP. I know that's not quite what he said, but I think the point is
arguable.
-Chris
I didn't say that recently, but trying to solve that problem indeed was
one of the major impulses that led me to FP.
Regards,
Jo
>> For example, in a langue where everything
>> is an object, what one is really asking for is for everything to be
>> first-class. Something I think most FPers will appreciate.
> Yes, I fully agree. I found that point on the list a bit odd myself.
It doesn't make sense before you define "object". And I suspect the
sentence really means "only objects are first class" and
consequently everything must be shoehorned into objects to be useful.
Come to think of it, a lot of the OO patterns (functor, factory,
visitor...?) seem to be recipes for doing exactly that.
In mucking with the translations, I've come to think that ML functors
are a better representation of classes and the instantiated objects.
Functors themselves are a sort of class mechanism. The call to the
functor is like the new operation where an environment is cut with
methods and state (aka object).
The hassle with using modules, though, is that modules can not be
passed to functions (you end up having to use functors for everything
to pass around the objects). Personally, I think that if a module
could be used as parameter to functions, there'd be much less need to
create syntactic extensions for OOP constructs. But I'm assuming that
the interaction with the module language has implications when trying
to pass around environments?
Just a thought,
Chris Rathman
"Chris Rathman" <Chri...@aol.com> writes:
> Personally, I think that if a module could be used as parameter to
> functions, there'd be much less need to create syntactic extensions
> for OOP constructs. But I'm assuming that the interaction with the
> module language has implications when trying to pass around
> environments?
I think the only reasons why ML distinguishes structures from records
and functors from functions is the type system. With dynamic typing
it's trivial to make them first-class.
As far as I can see, first-class modules completely subsume objects.
>>But I'm assuming that the interaction with the
>>module language has implications when trying to pass around
>>environments?
>
> I think the only reasons why ML distinguishes structures from records
> and functors from functions is the type system. With dynamic typing
> it's trivial to make them first-class.
Definitely. Operationally, the module language is just a lambda calculus
with records. However, it uses a form of dependent typing, and hence
making it first-class would immediately render it undecidable (since
there is non-termination). There is some middle ground, but it would
still complicate the type system of the core language and break type
inference. So for pragmatic reasons, this step is avoided.
My ideal language would in fact be one that makes the core language
expressive enough to subsume modules. But it is not at all clear how to
achieve that without losing most of the desired properties of the type
system. Scala is an interesting point in the spectrum. So is Cayenne.
Here's a somewhat different perspective... To me an object is just a special
form of tuple.
I use the following terminology:
member = element of the underlying tuple
method = a member which is a function
class = the type of an object
The only difference between an object and a regular tuple is that the value
of each method is immutable and fixed for all objects of a given class.
A class is given by:
- the type of the underlying tuple
- the fixed values of the methods
I would say a language is object-oriented iff it includes built-in support
for objects as defined above.
Sam
> member = element of the underlying tuple
> method = a member which is a function
> class = the type of an object
>
> The only difference between an object and a regular tuple is that the value
> of each method is immutable and fixed for all objects of a given class.
It's not that simple because... well, the reason can be stated in two ways:
- a method is different depending on which object it's taken from,
- a method has access to the object it's taken from.
Without that, almost all modern languages are OO, and definitely all
functional languages, because they all support records and first-class
functions.
This, and inheritance, are significant complications, and what actually
distinguishes various models of OO.
OO languages typically provide support for changing a method such that
all references to this method from other methods start calling the new
method. It's not that easy...
This still fits my model. An obvious way of encoding such methods is to take
the first argument to be the object. (x.foo(y) is just syntactic sugar for
foo(x, y)). The model also supports static methods - which don't have access
to the object.
> Without that, almost all modern languages are OO, and definitely all
> functional languages, because they all support records and first-class
> functions.
Ah... but that's not enough. How can we enforce the constraint that the
methods are fixed for any given class? Indeed, how can we distinguish
distinct classes, whose underlying record type is the same? It might be
possible to implement a pretty good approximation using dependent types, but
I am doubtful as to whether it's possible in SML or Haskell. In any case, it
wouldn't fit my definition of OO, as it could hardly be called 'built-in
support for objects'. (Of course, any programming construct could be
simulated in any Turing complete language, but that's very different from
providing native support.)
> This, and inheritance, are significant complications, and what actually
> distinguishes various models of OO.
I see no reason why my model cannot be extended to describe various flavours
of inheritance. The subtyping relation we would obtain would be somewhat
strange - a combination of record subtyping with the additional facility for
subtypes to arbitrarily change some of the methods - but that's not
particularly surprising as subtyping classes is a bit strange.
> OO languages typically provide support for changing a method such that
> all references to this method from other methods start calling the new
> method. It's not that easy...
The OO languages I'm familiar with only allow a method to be changed by
changing the class (i.e. by deriving from it).
Sam
>The OO languages I'm familiar with only allow a method to be changed by
>changing the class (i.e. by deriving from it).
Then you've never seen the powers that are CLOS.
mkb.
Hmm... there aren't many languages that make types first-class citizens :-)
(Not that this would be impossible. I've been toying with that idea for
a few years now.)
Regards,
Jo
Actually, when I said objects were tuples I should have said records.
Without subtyping, tuples and records are isomorphic (assuming the existence
of a total order on labels), but it's a bit difficult to define record
subtyping on tuples!
Also, plain inheritance just requires record subtyping. It's overriding that
depends on being able to change methods.
>> OO languages typically provide support for changing a method such that
>> all references to this method from other methods start calling the new
>> method. It's not that easy...
>
"Matthias Buelow" <m...@incubus.de> wrote in message
news:868y0pl...@drjekyll.mkbuelow.net...
> "Sam Lindley" <samar...@yahoo.co.uk> writes:
>
>>The OO languages I'm familiar with only allow a method to be changed by
>>changing the class (i.e. by deriving from it).
>
> Then you've never seen the powers that are CLOS.
Correct. I just googled for it and skimmed through Jeff Dalton's brief
guide:
http://www.aiai.ed.ac.uk/~jeff/clos-guide.html
As I understand it, classes in CLOS are first class citizens, and methods
can be changed in classes, but not directly in individual objects (correct
me if I'm wrong). I think all that needs modifying in my model is the
requirement that methods be immutable. They need not be immutable, but they
can only be modified by changing the class.
I'm reminded of an entertaining paper from FOOL 2001 which proposed a
mechanism for metamorphosing objects. The example involved changing a Frog
object into a Prince object :-)
"Joachim Durchholz" <j...@durchholz.org> wrote in message
news:da5q24$lpe$1...@online.de...
> Sam Lindley wrote:
[..]
>> A class is given by:
>> - the type of the underlying tuple
>> - the fixed values of the methods
>
> Hmm... there aren't many languages that make types first-class citizens
> :-)
My model doesn't require types to be first-class citizens - unless (as in
CLOS) classes are first-class citizens.
Anyway, here's a revised definition:
An object is a record subject to certain constraints and a class is the type
of an object.
member = field of the underlying record
method = a member which is a function
object = a record such that each method is fixed for all objects of a
given class
class = a pair of
- the type of the underlying record
- the fixed values of the methods
A language is object-oriented iff it includes built-in support for objects.
Sam
This is well-known and there are papers investigating different
encodings of objects along these lines (e.g. by Pierce). Abadi &
Cardelli argued that all these encodings have limits, at least with
respect to subtyping and variance. That was the motivation for their
object calculi.
> > Without that, almost all modern languages are OO, and definitely all
> > functional languages, because they all support records and first-class
> > functions.
>
> Ah... but that's not enough. How can we enforce the constraint that the
> methods are fixed for any given class? Indeed, how can we distinguish
> distinct classes, whose underlying record type is the same?
Counter question: why should we? In fact, I consider such nominal
typing one of the major weaknesses of (most) typed OO languages. It
severely hampers modularity and reuse. Cf. advantages of "duck typing".
- Andreas
Good. I'd have been surprised if this idea hadn't been investigated before.
I'd be interested to see what the problems with subtyping and variance
are... One of these days I'll get round to reading up on object calculi.
>> > Without that, almost all modern languages are OO, and definitely all
>> > functional languages, because they all support records and first-class
>> > functions.
>>
>> Ah... but that's not enough. How can we enforce the constraint that the
>> methods are fixed for any given class? Indeed, how can we distinguish
>> distinct classes, whose underlying record type is the same?
>
> Counter question: why should we? In fact, I consider such nominal
> typing one of the major weaknesses of (most) typed OO languages. It
> severely hampers modularity and reuse. Cf. advantages of "duck typing".
I quite agree. Unfortunately, the designers of most mainstream OO languages
appear to see it as a strength.
Sam
>> Counter question: why should we? In fact, I consider such nominal
>> typing one of the major weaknesses of (most) typed OO languages. It
>> severely hampers modularity and reuse. Cf. advantages of "duck typing".
>
> I quite agree. Unfortunately, the designers of most mainstream OO languages
> appear to see it as a strength.
>
As do physicists - YMMV. Personally, I've found it useful precisely
because it aids me in using duck typing for refactoring purposes - it lets
me type on what the duck can do rather than what its innards look like.
Sometimes you gotta fight fire with fire. Most
of the time you just get burnt worse though.
I'm afraid I don't follow. Can you repeat this in less poultry-oriented
terms?
Cheers,
- Andreas
> Sam Lindley schrieb:
>
>>Indeed, how can we distinguish
>>distinct classes, whose underlying record type is the same?
>
> Counter question: why should we? In fact, I consider such nominal
> typing one of the major weaknesses of (most) typed OO languages. It
> severely hampers modularity and reuse.
Counter-counter question: how do we otherwise differentiate between
"real" and "temperature"?
> Cf. advantages of "duck typing".
What's that? (URLs are OK.)
Regards,
Jo
>> Cf. advantages of "duck typing".
> What's that? (URLs are OK.)
Presumably this:
http://en.wikipedia.org/wiki/Duck_typing
---[excerpt]---
Duck typing
From Wikipedia, the free encyclopedia.
Duck typing is a humorous way of describing the type non-checking
system typical of some programming languages, such as with Smalltalk,
where the variable value itself determines what it can do. The name
combines two metaphors, both to make a serious point, and to make a
joke.
One could say that the language ducks the issue of typing. Initially
coined by Dave Thomas in the Ruby programming language community, its
premise is that if it walks like a duck, and talks like a duck, then
it is a duck.
Duck typing also refers to the concept in some languages that as long
as an object implements a certain interface, it is interchangeable
with any other object that implements the same interface, no matter
whether the two have a related inheritance hierarchy.
---[excerpt]---
Most likely their interface (i.e. their object type) will differ anyway.
For example, temperature certainly has conversion functions for degrees
Celsius and Fahrenheit.
But apart from that, I am not suggesting giving up on type abstraction.
I just suggest that coupling it with object types by requiring *every*
object type to be nominal is a bad idea. For example, I really see no
point in having nominal interface types like in Java. Classes are for
type abstraction, but interfaces aren't. As long as they are nominal you
could as well use classes.
> Joachim Durchholz wrote:
>
>>> Counter question: why should we? In fact, I consider such nominal
>>> typing one of the major weaknesses of (most) typed OO languages. It
>>> severely hampers modularity and reuse.
>>
>> Counter-counter question: how do we otherwise differentiate between
>> "real" and "temperature"?
>
> Most likely their interface (i.e. their object type) will differ anyway.
> For example, temperature certainly has conversion functions for degrees
> Celsius and Fahrenheit.
There are other differences (say, there's no way to assign a meaning to
the product of two temperatures - btw. that's a difference between reals
and any physical type: the product of two types isn't the same type as
the factors, it's of a new type: you can't add the product of two
lengths to a length, but you can add the product of two reals to a real).
I admit it was a bad example :-), but I think there are cases where two
types differ but not by signature. For example, when handling
database/TCP/whatever error codes, it makes sense to put each into a
separate type, but the signatures are the same (convert-to-string,
assignment, and comparison).
> But apart from that, I am not suggesting giving up on type abstraction.
> I just suggest that coupling it with object types by requiring *every*
> object type to be nominal is a bad idea. For example, I really see no
> point in having nominal interface types like in Java. Classes are for
> type abstraction, but interfaces aren't. As long as they are nominal you
> could as well use classes.
Not in Java - they are there to provide a substitute for Java's
nonexistent support of multiple inheritance.
Regards,
Jo
> >> Counter-counter question: how do we otherwise differentiate between
> >> "real" and "temperature"?
> >
> > Most likely their interface (i.e. their object type) will differ anyway.
> > For example, temperature certainly has conversion functions for degrees
> > Celsius and Fahrenheit.
>
> There are other differences (say, there's no way to assign a meaning to
> the product of two temperatures - btw. that's a difference between reals
> and any physical type: the product of two types isn't the same type as
> the factors, it's of a new type: you can't add the product of two
> lengths to a length, but you can add the product of two reals to a real).
Gentlemen, let's forget about the fact that you are eons away from any
many-language ray tracers...
But, please, leave physical units in peace, unles you want REALLY
discuss them. This is a delicate issue, much *beyond* the typing
problems. Of course you can multiply temperatures, or divide an energy
by temperature. Physical dimensions are not types, since there is
plenty of unit conventions around us. You *CAN* add centimeters and
seconds, if you take - as physicicts do quite often - that the
universal constant, the speed of light is equal to 1. There is no
"fundamental typing" of energy, of force or whatever.
You are not interested in physics, so, please, don't invade their
territorial waters, full of sharks, and awful medusas (not only
of ducks...).
Jerzy Karczmarczuk
--
Posted via Mailgate.ORG Server - http://www.Mailgate.ORG