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. |