deech / fltkhs

Haskell bindings to FLTK GUI toolkit.
MIT License
190 stars 24 forks source link

Use phantom types to model class hierarchies? #9

Open HeinrichApfelmus opened 9 years ago

HeinrichApfelmus commented 9 years ago

Since you've asked for feedback. :smile:

As far as I understand, you've used type-level computations to solve the problem of modeling the FLTK class hierarchy. This has some issues; for instance, I have to increase GHC's context stack signifcantly in order to successfully compile the example programs. I have to admit that I don't understand how it works in detail, but would it also be possible to solve this problem using phantom types?

Daan Leijens's article on the design of WxHaskell (section 5) explains the use of phantom types for modeling an OOP class hierarchy. It has the tremendous benefit of yielding simple type signatures. Of course, it cannot deal with all situations, like multiple inheritance. But the radical simplicity may be totally worth it. Is that an option?

deech commented 9 years ago

Sorry for the length of this response.

It's funny that you bring up WxHaskell's approach because that was the first avenue I explored mostly due to your reply [1] on haskell-cafe. It is indeed an elegant and simple but I ended up having to abandon it. The reason is suppose there is a class hierarchy in some OO language:

class Shape
   - int area
class Rectangle : Shape
   - int area (int, int)
class Square : Rectangle
   - int area (int)

Using the wxHaskell approach I can easily encode Rectangle's area but Square's implementation is impossible. I found a number of cases in the FLTK hierarchy where the type signature of a subclass function did not match the parent's.

Using the scheme in the current codebase you can call area <instance-of-Rectangle> 1 2, and area <instance-of-Square> 2, but area <instance-of-Rectangle> 1 will cause a compile time error asking for another argument. The error itself is quite readable. Note also that one does not have to qualify the area function call with Rectangle.area or Square.area, the correct implementation is determined solely by the type of the <instance-of-*> argument. Additionally this scheme is open, Rectangle could easily be split off into it's own package.

Having to raise the context stack is unfortunate. I do this because in working out which implementation of a function to dispatch a call to, the type system has to iterate down all the possible functions available to that "instance" and if there are over a certain number that can cause a context stack overflow.

I did have a type level function that used an type-level fixed-size buffer to keep the number of iterations lower, but could not get it below the default limit of 21, and it was not constant, and the code was ugly. Since I ended up having to raise the limit anyway so I figured the simpler less efficient implementation would be fine since the hit only happens at compile time and today's hardware can handle it. There is also talk among the GHC devs about raising the limit to something less ridiculous (like 1024).

Thanks for taking the time to write feedback!

[1] https://mail.haskell.org/pipermail/haskell-cafe/2013-September/110203.html

HeinrichApfelmus commented 9 years ago

Ah, I see. Yeah, that would be one of the cases where the phantom type approach fails. But are you sure that this complexity is really needed? I mean, it is your call to make, but personally, I would be quite happy to use different names, say area and areaSquare for functions that have different numbers of arguments. In a way, you are implementing polymorphic records, and I am not entirely sure whether they are worth the complexity in this context. When writing a GUI, I would be more than happy to learn straightforward Haskell 98 type signatures, even if they differ from the C++ headers in some cases. After all, my focus would be on writing a GUI in Haskell, not so much on translating C++ code 1:1. Of course, that is just my personal taste, feel free to disregard it. :smile:

deech commented 9 years ago

That's a great point and I struggled with it. In fact if you look back in the commits I had the wxHaskell model almost fully implemented but had so many special functions like setItemMenuPrim, setItemBoxedMenu etc. that I aesthetically couldn't take it anymore. I barely wanted to use my own API. :)

As a way of penance for the complexity the Haddocks for all the widget modules show the functions with the type signatures the user would care about rather than the scary complex ones. It's not an amazing solution but it's the best I could come up with.

The context stack is the only piece of ugliness I couldn't totally hide from the end-user. Hopefully either GHC devs will increase the default stack size or someone cleverer than I will figure out how to keep the stack constant size and under 21.

Also this scheme relies heavily on phantom types and empty datatypes and is heavily inspired by wxHaskell, so I thank you for pointing me that way

HeinrichApfelmus commented 9 years ago

Well, WxHaskell also has a couple of functions where this is an issue. The solution there was to make type classes for these cases. For instance, the Text class works for subclasses of both the MenuItem and Window classes, even though they have no common ancestor.