Extensible records (was Re: Existential types, object-orientation.)

Jan Krynicky Jan@chipnet.cz
Sun, 09 Nov 1997 14:15:13 -0800


Nick Kallen wrote:
> 
> Will you be able to use type classes with dynamic types? (the answer is
> probably yes, but i'm just making sure).
> 
> I'd like to request this feature (related to dynamic types) that I think
> would be useful. (I assume this is not already planned to be implemented):
> 
> There should be a way to specify a Dynamic type class like (in english):
> "Dynamic class Drawable: A record with a field position, and a function
> draw." The reason this is distinct from an existential type is that these
> should be extendable. So some Dynamic type Sphere can be a member of the
> Drawable class and can have --additional fields and functions-- like radius,
> volume, render, etc. I'm getting very repetitive, but this would allow one
> to write functions like a clipboard Copy function that could copy any
> drawable, no matter what fields and functions it has, as long as it has the
> base stuff that defines the class.


I think that to allow true OOP in cleant we should add one more feature
:
Extensible records. I mean something like

 :: Point = {x :: Real;y :: Real}

 :: Circle = { Point & r :: Real}


We could then write functions working not only on Points but also
on other types created by extending Point. The implementation should
be pretty simple (apart from type checking).

If records are implemented as structs in C, that is the items are 
stored in memory in the order they appear in the definition, we just
may look up the x at the same place in any record derived from Point.
All we have to change is the copying of records, we have to remember
the size so that we may copy the right amount of data.

We should also be able to construct the derived objects by
extending the parrent object :

 p :: Point
 p = {x = 5.7; y = 2.3}
 c :: Circle
 c = {p & r = 1.1}


Now lets look at posible types of function move :

1)

 move (dx,dy) {x,y} = {dx+x,dy+y}

 I think the most general type would be
  move :: (Real,Real) a -> Point | a is Point
   (OK, we would probably have to give compiler a hint :-)

  where "| a is Type" means that a is Type or any type derived from
Type.

 If we define
  p::Point
  p = { x=3.0; y=3.0}

  c1::Circle
  c1 = { x=3.0; y=3.0; r=1.5}

  :: OtherCircle = { x :: Real; y :: Real; r :: Real}
  c2::OtherCircle
  c2 = { x=3.0; y=3.0; r=1.5}

 we may call the function as

  p1=move (2.0,4.0) p   // p1 :: Point
  p2=move (2.0,4.0) c1  // p2 :: Point

 but for ease of implementation I wouldn't allow

  p3=move (2.0,4.0) c2

 OtherCircle is not a subtype of Point. OK?
    

2) 
 move (dx,dy) r=:{x,y} = { r & x = dx + x,dy + y}

 Now this is a bit different, we do not construct a new record, we
change
 the object we get, so the type is :

  move :: (Real,Real) a -> a | a is Point


3)
 move {dx, dy} r=:{x,y} = { r & x = dx + x,dy + y}
 
  move :: a b -> b | a is Point & b is Point


It would be great if the compiler would be able to overload such
functions
so that we would be able to use something like

draw :: a -> [Picture -> Picture] | a is Point
draw {x,y} = [DrawPoint (x,y)]

draw :: a -> [Picture -> Picture] | a is Circle
draw {x,y,r} = [DrawCircle (x,y) r]

If we do not allow multiple inheritance it shouldn't be that hard to
find out 
which version should be used.

Even if the compiler is not able to do this we may simulate it by
storing methods in the record.

If we use simple.

 :: Point = {x :: Real;y :: Real;draw :: Point -> [Picture -> Picture]}

newPoint (setx,sety) = {x = setx; y = sety; draw = draw_}
 where
  draw_ {x,y} = [DrawPoint (x,y)]


Draw point = point.draw point
  // this is just a wraper. It would be nice if compiler would
  // give us some syntactic sugar : point->draw  == point.draw point
       point->move (3.3,2.2) == point.move point (3.3,2.2)

and
 :: Circle = {Point & r :: Real}

we may not use

newCircle (setx,sety,setr) = {x = setx; y = sety; draw = draw_; r =
setr}
 where
  draw_ {x,y,r} = [DrawCircle (x,y) r]

because the draw_ has type :: Circle -> [Picture -> Picture].
Notice that changing Point to 
 Point :: {x :: Real;y :: Real;draw :: a -> [Picture -> Picture] | a is
Point}
is of no help.

BUT now that we (will) have Dynamic we may use

 Point :: {x :: Real;y :: Real;draw :: Dynamic -> [Picture -> Picture]}
 newPoint (setx,sety) = {x = setx; y = sety; draw = draw_}
  where
   draw_ ({x,y} :: Point) = [DrawPoint (x,y)]
    // or maybe :: a | a is Point ?

 :: Circle = {Point & r :: Real}
 newCircle (setx,sety,setr) = {x = setx; y = sety; draw = draw_; r =
setr}
  where
   draw_ ({x,y,r} :: Circle) = [DrawCircle (x,y) r]

 If you want the inheritance you may use something like

 newCircle (setx,sety,setr) = c`
  where
   c = newPoint (setx,sety)
   c` = {c & r = setr; draw = draw_}
   draw_ (circle=:{x,y,r} :: Circle) = [DrawCircle (x,y) r] ++ c.draw
circle



What we have now are objects just like they have in Pascal.
We lack only the multiple inheritance to get the C++ objects.


There is only one last affair we have to take care of, uniqueness.
If we look at the definition of the Draw wrapper,
(   Draw point = point.draw point  )
we may see that the point may NOT be unique.


p :: *Point
drlist = Draw p

would not work. It is a big problem, maybe somebody knows
how to solve it in todays Clean, but I don't.

The nicest solution would be to extende the definition of records
so that we could tag some fields a read-only, that is
you may set them once when creating the record
and then any {record & rdonly-field = something}
would cause an compiletime error. If I tag the method read only,
"c` = c.draw c" is OK even with unique "c".



Someone may point out that we may use something like

 Draw :: Dynamic -> [Picture -> Picture]
 Draw ({x,y} :: Point) = [DrawPoint (x,y)]
 Draw ({x,y,r} :: Circle) = [DrawCircle (x,y) r]
 ...

BUT :
 1. Will I be able to extend the Draw definition later?
    I know I could use something like
     MyDraw (x :: MyObject) = ...
     MyDraw (x) = Draw x
    but everybodu calling the old Draw would be still unable to 
    draw my objects.

 2. What about the inheritation? All that comes to my mind is
    to construct a new object of that type and call Draw again.
    This seems to expensive.

     
     Draw ({x,y} :: Point) = [...]
     Draw ({x,y,r} :: Circle) = Draw p & [...]
      where
       p :: Point
       p = {x,y}

 3. This would pretty much kill any typechecking. It is good
    to be able to bypass it, but we should do it only if realy
necessary.


What do you think?

Jenda