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".
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.
Subtyping describes type relationships, and subtype polymorphism enables operations defined for supertypes to be safely substituted with subtypes.
in a simple word: subtyping and inheritance both are polymorphism, (inheritance is a dynamic polymorphism - overriding).
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.
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 ReactiveWidget
s 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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With