
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.