[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