[GUI] inheritance using type classes

Daan Leijen daanleijen at xs4all.nl
Wed Sep 17 13:03:43 EDT 2003


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 
inheritance using type classes versus phantom types.)

Alle 22:18, martedì 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 
> type classes to model the inheritance but that leads to a) other 
> dependencies on extensions, b) hard to understand error messages, 
> and c) a much more complex model. (See Andre Pang's master thesis 
> for a ingenious way to model full inheritance)

Inheritance can be modelled fully with type classes but leads to a 
complicated system that depends on many extensions to work in practice 
(like MPTC and functional dependencies). That is why I used phantom 
types to model inheritance in wxHaskell -- simplicity! 

However, I just realized that with the proper restrictions, there also 
exists a reasonably simple inheritance model using just haskell98 type 
classes. (There is a catch of course, but more on that later). Most 
complications normally arise as we also want to model object methods 
that are overloaded on their type signatures (like java and c++ 
allow). For wxHaskell though, we don't need to do this, as no such 
overloading occurs.

This allows us to "lift" the object methods out of a haskell class 
declaration and to model only the inheritance relationship with type 
classes.

Here is how it works concretely: In the Haskell world, each object is 
in the end represented by a pointer, say "Addr". So, we can make a 
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 
method. First, we create a type that represents this class in Haskell, 
and we'll call "WindowObject".

> newtype WindowObject  = WindowObject Addr
>
> windowCreate :: IO WindowObject
> windowCreate = do{ addr <- primWindowCreate;
                     return (WindowObject addr) }

Next, we model the inheritance using a (phantom) type class and 
instance.

> class Object window => Window window
>
> instance Object WindowObject where
>   self (WindowObject addr) = addr
>
> instance Window WindowObject

The "show" method is written as:

> windowShow :: Window window => window -> IO ()
> windowShow window
>    = primWindowShow (self window)

Note that the inferred type is "Object w => w -> IO ()", but we want 
to constrain it by hand to windows only. Furthermore, we don't use a 
"WindowObject" as the argument, as we want to be able to pass *any* 
kind of Window to this function.

For example, a Button object derives from a Control, that in turn 
derives from the Window class:

> newtype ButtonObject  = ButtonObject Addr
>
> class Control button => Button button
>
> instance Object ButtonObject where
>   self (ButtonObject addr) = addr
>
> instance Window ButtonObject
> instance Control ButtonObject
> instance Button ButtonObject
>
> buttonCreate :: IO ButtonObject
> ...
> buttonSetLabel :: Button button => button -> String -> IO ()
> ...

Clearly, we can now use the "windowShow" method on "ButtonObjects" too 
-- exactly what we want. Furthermore, we can also downcast objects:

> downcastWindow :: Window window => window -> WindowObject
> downcastWindow window  = WindowObject (self window)

Up till now, there is not much advantage with regard to using phantom 
types: the effect is the same and the types with overloading are bit 
more complicated. However, there may be some other advantages.

Suppose I want to create more abstraction and use a type class 
"Textual" that retrieves some text from an object. 

> class Textual w where
>   text :: w -> IO String

We use a type class here since we want to use "text" on both the 
objects imported from the library, but also on user defined data 
types. Suppose that every window has "windowGetLabel" method. This 
means that we define "Textual" for any kind of window in one go:

> instance Window w => Textual w where
>   text = windowGetLabel

I use the same trick when using phantom types (the free "a" type 
variable encodes the "any kind of window"):

> instance Textual (Window a) where
>   text = windowGetLabel

The attentative reader now notices that both instance declarations are 
illegal in haskell98 and require the "allow undecidable instances" 
flag (i.e. the context reducer could loop). Furthermore, the latter 
also uses a type synonym but that is easy to circumvent (Window a == 
Ptr (CWxObject (CEvtHandler (CWindow a)))

However, there is an important difference -- If we really want, we 
could replace the first instance declaration with a specific instance 
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 
done with phantom types (as the instance heads remain "complex", even 
though they are always "decidable" (i.e. reduce without looping)).

Given the more complex type signatures and error messages, I 
personally find the price too high. Especially since in the wxhaskell 
case the amount of instance declarations would rise from a single 
declaration to hundreds of instance declarations for each kind of 
widget. But maybe this gives at least some more insight in the 
different trade-offs that we can make.

All the best,
 Daaan.




More information about the GUI mailing list