|
From: Daan L. <daa...@xs...> - 2003-09-17 10:03:52
|
Hi all,
(Sorry for the long mail and pollution of the mailing list, but
I would like to say a bit more about the trade-offs when modelling=20
inheritance using type classes versus phantom types.)
Alle 22:18, marted=EC 16 settembre 2003, Daan Leijen ha scritto:
> This is a devious thing to do, but totally unavoidable given the way
> I model inheritance with phantom types. I have considered using=20
> type classes to model the inheritance but that leads to a) other=20
> dependencies on extensions, b) hard to understand error messages,=20
> and c) a much more complex model. (See Andre Pang's master thesis=20
> for a ingenious way to model full inheritance)
Inheritance can be modelled fully with type classes but leads to a=20
complicated system that depends on many extensions to work in practice=20
(like MPTC and functional dependencies). That is why I used phantom=20
types to model inheritance in wxHaskell -- simplicity!=20
However, I just realized that with the proper restrictions, there also=20
exists a reasonably simple inheritance model using just haskell98 type=20
classes. (There is a catch of course, but more on that later). Most=20
complications normally arise as we also want to model object methods=20
that are overloaded on their type signatures (like java and c++=20
allow). For wxHaskell though, we don't need to do this, as no such=20
overloading occurs.
This allows us to "lift" the object methods out of a haskell class=20
declaration and to model only the inheritance relationship with type=20
classes.
Here is how it works concretely: In the Haskell world, each object is=20
in the end represented by a pointer, say "Addr". So, we can make a=20
class that returns this pointer for each haskell object.
> class Object object where
> self :: object -> Addr
Now, suppose we have a "Window" class with a creation method and show=20
method. First, we create a type that represents this class in Haskell,=20
and we'll call "WindowObject".
> newtype WindowObject =3D WindowObject Addr
>
> windowCreate :: IO WindowObject
> windowCreate =3D do{ addr <- primWindowCreate;
return (WindowObject addr) }
Next, we model the inheritance using a (phantom) type class and=20
instance.
> class Object window =3D> Window window
>
> instance Object WindowObject where
> self (WindowObject addr) =3D addr
>
> instance Window WindowObject
The "show" method is written as:
> windowShow :: Window window =3D> window -> IO ()
> windowShow window
> =3D primWindowShow (self window)
Note that the inferred type is "Object w =3D> w -> IO ()", but we want=20
to constrain it by hand to windows only. Furthermore, we don't use a=20
"WindowObject" as the argument, as we want to be able to pass *any*=20
kind of Window to this function.
For example, a Button object derives from a Control, that in turn=20
derives from the Window class:
> newtype ButtonObject =3D ButtonObject Addr
>
> class Control button =3D> Button button
>
> instance Object ButtonObject where
> self (ButtonObject addr) =3D addr
>
> instance Window ButtonObject
> instance Control ButtonObject
> instance Button ButtonObject
>
> buttonCreate :: IO ButtonObject
> ...
> buttonSetLabel :: Button button =3D> button -> String -> IO ()
> ...
Clearly, we can now use the "windowShow" method on "ButtonObjects" too=20
-- exactly what we want. Furthermore, we can also downcast objects:
> downcastWindow :: Window window =3D> window -> WindowObject
> downcastWindow window =3D WindowObject (self window)
Up till now, there is not much advantage with regard to using phantom=20
types: the effect is the same and the types with overloading are bit=20
more complicated. However, there may be some other advantages.
Suppose I want to create more abstraction and use a type class=20
"Textual" that retrieves some text from an object.=20
> class Textual w where
> text :: w -> IO String
We use a type class here since we want to use "text" on both the=20
objects imported from the library, but also on user defined data=20
types. Suppose that every window has "windowGetLabel" method. This=20
means that we define "Textual" for any kind of window in one go:
> instance Window w =3D> Textual w where
> text =3D windowGetLabel
I use the same trick when using phantom types (the free "a" type=20
variable encodes the "any kind of window"):
> instance Textual (Window a) where
> text =3D windowGetLabel
The attentative reader now notices that both instance declarations are=20
illegal in haskell98 and require the "allow undecidable instances"=20
flag (i.e. the context reducer could loop). Furthermore, the latter=20
also uses a type synonym but that is easy to circumvent (Window a =3D=3D =
Ptr (CWxObject (CEvtHandler (CWindow a)))
However, there is an important difference -- If we really want, we=20
could replace the first instance declaration with a specific instance=20
declaration for each kind of object:
> instance Textual WindowObject
> instance Textual ControlObject
> instance Textual ButtonObject
> ....
This would be haskell98 compliant. Unfortunately, no such thing can be=20
done with phantom types (as the instance heads remain "complex", even=20
though they are always "decidable" (i.e. reduce without looping)).
Given the more complex type signatures and error messages, I=20
personally find the price too high. Especially since in the wxhaskell=20
case the amount of instance declarations would rise from a single=20
declaration to hundreds of instance declarations for each kind of=20
widget. But maybe this gives at least some more insight in the=20
different trade-offs that we can make.
All the best,
Daaan.
|