Newsgroups: comp.lang.ada,comp.lang.c++,comp.object,comp.lang.clos
Path: cantaloupe.srv.cs.cmu.edu!das-news2.harvard.edu!news2.near.net!howland.reston.ans.net!pipex!uknet!comlab.ox.ac.uk!sable.ox.ac.uk!lady0065
From: lady0065@sable.ox.ac.uk (David Hopwood)
Subject: Re: Multiple dispatch
Message-ID: <1995Apr26.163942.24260@inca.comlab.ox.ac.uk>
Followup-To: comp.object,comp.lang.ada,comp.lang.clos
Sender: david.hopwood@lmh.ox.ac.uk
Organization: Oxford University, England
References: <dewar.797512974@gnat> <EACHUS.95Apr17162921@spectre.mitre.org> <3n1als$ksi@no-names.nerdc.ufl.edu> <EACHUS.95Apr19194215@spectre.mitre.org>
Date: Wed, 26 Apr 95 16:39:42 BST
Lines: 114
Xref: glinda.oz.cs.cmu.edu comp.lang.ada:29179 comp.lang.c++:125140 comp.object:30025 comp.lang.clos:2897

[comp.lang.c++ removed from followups]

In article <EACHUS.95Apr19194215@spectre.mitre.org>,
Robert I. Eachus <eachus@spectre.mitre.org> wrote:
>In article <3n1als$ksi@no-names.nerdc.ufl.edu> kem@prl.ufl.edu (Kelly Murray)
>writes:
>
> > But we have a device that can do a draw-line real fast, so we want to
> > use it when we can, for example, on a square shape.  We must know that
> > a shape can provide the line information.  Thus, the draw method is
> > really specialized for not a square, but a lined-shaped object which
> > a square should inherit from, and return
>
> >  (defmethod draw ((dev line-device) (shape lined-shape))
> >    (dolist (line (shape-lines shape))
> >      (draw-line dev line)))
>
> > I believe this is basically the same strategy that Robert Eachus
> > has outlined.  I don't see how it could fit easily into a 
> > single-dispatch model.  Which object is doing the dispatching?
> > Do we make the device explicitly ask the shape at run-time 
> > if it can support a given operation? :
>
>   No.  I may not have explained it well.  Let me see if I can use
>your example to elucidate.  First, in Ada 95--and in some other
>OO-languages--objects may be passed as non-dispatching parameters,
>then used internal to the called procedure to dispatch on.  So you can
>end up doing two-level dispatching, in this case first on the shape,
>then on the device.
>
>   If you specified drawing a square in terms of drawing four lines:
>
>   procedure Draw(S: Square; L: Location; D: Device) is
>   begin
>      Draw_Line(Make_Line(S.Side,Horizontal),L, D);
>      Draw_Line(Make_Line(S.Side,Vertical),L, D);
>      Draw_Line(Make_Line(S.Side,Vertical),L+Horizontal_Displacement(S.Side),
>           D);
>      Draw_Line(Make_Line(S.Side,Horizontal),L+Vertical_Displacement(S.Side),
>           D);
>   end Draw;
>
>   If we defined Line and Point as part of the primitive set of
>operations for all devices, this is all we have to do to add drawing
>squares. ...
>   (Note that to make things neater, you probably have some
>definitions of the form:
>
>   procedure Draw(S: Line; L: Location; D: Device) is
>   begin Draw_Line(S,L,D); end Draw;
>
>   to do the outer dispatch where the shape to be drawn is a run-time
>parameter.  This is the "extra" bookkeeping required by not having
>multiple dispatch in the language.)

This seems to assume that there are exactly two levels of shapes: primitives,
and shapes built using primitives. It should be possible to make this a little
more flexible, without sacrificing the O(1) minimum effort (and no
recompilation) needed to create a new device or shape.

In practice, most shapes can be drawn in several ways, depending on what
primitives are available.

For example, ellipses can be drawn either as primitive, using splines, arcs,
line segments, or pixel-by-pixel. Each of these requires a different algorithm,
but they are all ellipse-drawing algorithms, and so it is reasonable to
associate all of them with the class Ellipse. This is how I might do it in
a single-dispatch language:

type Device
    pixel (s: Pixel, l: Location)
    -- optionally, other methods to draw specific primitives, eg.
    -- line (s: Line, l: Location)

type Shape
    draw (l: Location, d: Device)

class Ellipse
    conform-to Shape
    -- instance variables
    draw (l: Location, d: Device) is
        if (d.respondsTo (#ellipse))
            d.ellipse (self, l) -- cast if necessary (always succeeds)
        elseif (d.respondsTo (#spline))
            self.spline_ellipse (l, d)
        elseif (d.respondsTo (#arc))
            self.arc_ellipse (l, d)
        elseif (d.respondsTo (#line))
            self.line_ellipse (l, d)
        else -- all devices support pixels
            self.pixel_ellipse (l, d)
    protected spline_ellipse (l, d) is ...
    protected ...

class Line
    draw (l: Location, d: Device) is
        if (d.respondsTo (#line))
            d.line (self, l) -- cast if necessary (always succeeds)
        else
            self.pixel_line (l, d)
    protected pixel_line (l, d) is -- Bresenham's algorithm, say.

Note that although a line can be drawn on any device, Ellipse will only use
its line-drawing algorithm if it can draw lines as primitives. The minimum
needed to add a new shape is to check whether it is primitive, and to supply
one algorithm for drawing it.

All those elseifs might look ugly, but they don't compromise extensibility;
no existing code has to be changed if I add a new shape or a new device.
If you want to add a new algorithm for an existing shape, you can do it either
by changing the shape class or by inheriting from it.

David Hopwood
david.hopwood@lmh.ox.ac.uk
