Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Subtype polymorphism in Haskell

Building a hierarchy of GUI widget classes is pretty much a standard exercise in object-oriented programming. You have some sort of abstract Widget class, with an abstract subclass for widgets that can contain other widgets, and then you have a profusion of further abstract classes for widgets that support textual display, widgets that support being the input focus, widgets that have a boolean state, right down to actual concrete classes such as buttons, sliders, scrollbars, check boxes, etc.

My question is: What is the best way to do this in Haskell?

There are a number of things that make building a Haskell GUI difficult, but are not part of my question. Doing interactive I/O is mildly tricky in Haskell. Implementing a GUI almost always means writing a wrapper to an extremely low-level C or C++ library. And people writing such wrappers tend to copy the existing API verbatim (presumably so anybody who knows the wrapped library will feel at home). These problems do not interest me at the moment. I'm interested purely in how best to model subtype polymorphism in Haskell.

What kind of properties would we want from our hypothetical GUI library? Well, we want it to be possible to add new widget types at any time. (In other words, a closed set of possible widgets is no good.) We want to minimise code duplication. (There are a lot of widget types!) Ideally we want to be able to stipulate one specific widget type when necessary, but also to be able to handle collections of any widget type if needed.

All of the above is of course trivial in any self-respecting OO language. But what is the best way to do this in Haskell? I can think of several approaches, but I'm not sure which one would be "best".

like image 328
MathematicalOrchid Avatar asked Aug 17 '12 09:08

MathematicalOrchid


People also ask

Does Haskell have subtypes?

Haskell has type classes, you will seriously want to use something like that very very quickly. And you can for the most part. You can create a Shape type class and put all the draw and area and perimeter and what not in it.

What is the purpose of subtype polymorphism?

Subtyping describes type relationships, and subtype polymorphism enables operations defined for supertypes to be safely substituted with subtypes.

Is subtype polymorphism inheritance?

in a simple word: subtyping and inheritance both are polymorphism, (inheritance is a dynamic polymorphism - overriding).

What is parametric polymorphism and subtype polymorphism?

1 Parametric polymorphismA polymorphic datatype is one that can contain elements of different types. Several kinds of polymorphism are commonly used in modern languages. • Subtype polymorphism gives a single term many types using the subsumption rule.


2 Answers

Having actual widget objects is something that's very object-oriented. A commonly used technique in the functional world is to instead use Functional Reactive Programming (FRP). I'll briefly outline what a widget library in pure Haskell would look like when using FRP.


tl/dr: You don't handle "Widget objects", you handle collections of "event streams" instead, and don't care from which widgets or where those streams come from.


In FRP, there's the basic notion of an Event a, which can be seen as an infinite list [(Time, a)]. So, if you want to model a counter that counts up, you'd write it as [(00:01, 1), (00:02, 4), (00.03, 7), ...], which associates a specific counter value with a given time. If you want to model a button that is being pressed, you produce a [(00:01, ButtonPressed), (00:02, ButtonReleased), ...]

There's also commonly something called a Signal a, which is like an Event a, except that the modeled value is continuous. You don't have a discrete set of values at specific times, but you can ask the Signal for its value at, say, 00:02:231 and it will give you the value 4.754 or something. Think of a signal as an analogue signal like the one on a heart charge meter (electrocardiographic device/Holter monitor) at a hospital: it's a continuous line that jumps up and down but never makes a "gap". A window does always have a title, for example (but perhaps it's the empty string), so you can always ask it for its value.


In a GUI library, on a low level, there'd be a mouseMovement :: Event (Int, Int) and mouseAction :: Event (MouseButton, MouseAction) or something. The mouseMovement is the actual USB/PS2 mouse output, so you only get position differences as events (e.g. when the user moves the mouse up, you'd get the event (12:35:235, (0, -5)). You'd then be able to "integrate" or rather "accumulate" the movement events to get a mousePosition :: Signal (Int, Int) that gave you absolute mouse coordinates. mousePosition could also take into consideration absolute pointing devices such as touch screens, or OS events that reposition the mouse cursor, etc.

Similarly for a keyboard, there'd be a keyboardAction :: Event (Key, Action), and one could also "integrate" that event stream into a keyboardState :: Signal (Key -> KeyState) that lets you read a key's state at any point in time.


Things get more complicated when you want to draw stuff onto the screen and interact with widgets.

To create just a single window, one would have a "magic function" called:

window :: Event DrawCommand -> Signal WindowIcon -> Signal WindowTitle -> ...        -> FRP (Event (Int, Int) {- mouse events -},                Event (Key, Action) {- key events -},                ...) 

The function would be magical because it would have to call the OS-specific functions and create a window (unless the OS itself is FRP, but I doubt that). That is also why it is in the FRP monad, because it would call createWindow and setTitle and registerKeyCallback etc in the IO monad behind the scenes.

One could of course group all of those values into data structures so that there would be:

window :: WindowProperties -> ReactiveWidget        -> FRP (ReactiveWindow, ReactiveWidget) 

The WindowProperties are signals and events that determine the look and behavior of the window (e.g. if there should be close buttons, what the title should be, etc.).

The ReactiveWidget represents S&Es that are keyboard and mouse events, in case you want to emulate mouse clicks from within your application, and an Event DrawCommand that represents a stream of things you want to draw on the window. This data structure is common to all widgets.

The ReactiveWindow represents events like the window being minimized etc, and the output ReactiveWidget represents mouse and keyboard events coming from the outside/the user.

Then one would create an actual widget, let's say a push button. It would have the signature:

button :: ButtonProperties -> ReactiveWidget -> (ReactiveButton, ReactiveWidget) 

The ButtonProperties would determine the color/text/etc of the button, and the ReactiveButton would contain e.g. an Event ButtonAction and Signal ButtonState to read the button's state.

Note that the button function is a pure function, since it only depends on pure FRP values like events and signals.

If one wants to group widgets (e.g. stack them horizontally), one would have to create e.g. a:

horizontalLayout :: HLayoutProperties -> ReactiveWidget                  -> (ReactiveLayout, ReactiveWidget) 

The HLayoutProperties would contain information about border sizes and the ReactiveWidgets for the contained widgets. The ReactiveLayout would then contain a [ReactiveWidget] with one element for each child widget.

What the layout would do is that it would have an internal Signal [Int] that determined the height of each widget in the layout. It would then receive all of the events from the input ReactiveWidget, then based upon the partition layout select an output ReactiveWidget to send the event to, meanwhile also transforming the origin of e.g. mouse events by the partition offset.


To demonstrate how this API would work, consider this program:

main = runFRP $ do rec -- Recursive do, lets us use winInp lazily before it is defined    -- Create window:   (win, winOut) <- window winProps winInp        -- Create some arbitrary layout with our 2 widgets:   let (lay, layOut) = layout (def { widgets = [butOut, labOut] }) layInp       -- Create a button:       (but, butOut) = button butProps butInp       -- Create a label:       (lab, labOut) = label labProps labInp       -- Connect the layout input to the window output       layInp = winOut       -- Connect the layout output to the window input       winInp = layOut       -- Get the spliced input from the layout       [butInp, layInp] = layoutWidgets lay       -- "pure" is of course from Applicative Functors and indicates a constant Signal       winProps = def { title = pure "Hello, World!", size = pure (800, 600) }       butProps = def { title = pure "Click me!" }       labProps = def { text = reactiveIf                               (buttonPressed but)                               (pure "Button pressed") (pure "Button not pressed") }   return () 

(def is from Data.Default in data-default)

This creates an event graph, like so:

     Input events ->            Input events -> win ---------------------- lay ---------------------- but \      <- Draw commands etc.  \   <- Draw commands etc.      | | Button press ev.                              \  Input events ->            | V                               \---------------------- lab /                                 <- Draw commands etc. 

Note that there doesn't have to be any "widget objects" anywhere. A layout is simply a function that transforms input and output events according to a partitioning system, so you could use the event streams you gain access to for widgets, or you could let another sub-system generate the streams entirely. The same goes for buttons and labels: they are simply functions that convert click events into draw commands, or similar things. It's a representation of complete decoupling, and very flexible in its nature.

like image 57
dflemstr Avatar answered Oct 11 '22 13:10

dflemstr


The wxHaskell GUI library makes excellent use of phantom types to model a widget hierarchy.

The idea is the following: all widgets share the same implementation, namely they are foreign pointers to C++ objects. However, this doesn't mean that all widgets need to have the same type. Instead, we can build a hierarchy like this:

type Object a = ForeignPtr a  data CWindow a data CControl a data CButton a  type Window  a = Object  (CWindow a) type Control a = Window  (CControl a) type Button  a = Control (CButton a) 

This way, a value of the type Control A also matches the type Window b, so you can use controls as windows, but not the other way round. As you can see, subtyping is implemented via a nested type parameter.

For more on this technique, see section 5 in Dan Leijen's paper on wxHaskell.


Note that this technique appears to be limited to the case where the actual representation of widgets is uniform, i.e. always the same. However, I am confident that with some thought, it can be extended to the case where widgets have different representations.

In particular, the observation is that object-orientation can be modeled by including the methods in the data type, like this

data CWindow a = CWindow     { close   :: IO ()     , ...     } data CButton a = CButton     { onClick :: (Mouse -> IO ()) -> IO ()     , ...     } 

Subtyping may save some boilerplate here, but it's not required.

like image 30
Heinrich Apfelmus Avatar answered Oct 11 '22 15:10

Heinrich Apfelmus