GUI programming in functional and logic languages tends to feel quite hard. There is nothing intrinsic in FP that makes GUI programming extra hard, but when everywhere else you are surrounded by nice, abstract, compositional, high-level descriptive definitions, working at the GUI’s low level feels… well, like you’re doing something wrong.
In the case of Haskell, for instance, and by using the IO monad and MVars, we know that code can be, at least, as good (or as ugly) as traditional imperative code. (The only additional difficulty might be, if at all, reasoning about IO and lazy evaluation, but the cases in which that presents an actual problem are rare).
GUI programming revolves around the idea of the widget (or gadget or visual element), which has internal state, responds to user events, presents some information, and takes “ownership” of a certain area of the screen. Widgets are placed on the screen in positions that depend on those of other widgets, and when one of them is affected by a user event,
several other widgets can see their states changed. Being such a state-driven transformation, GUIs have the word monad written all over them. But not all is bad…
When doing GUI programming in Haskell, there are two (main) options.
– To use some binding to an imperative library. There are elegant, wide-spread, imperative solutions for GUI programming, and some of them have corresponding binding libraries. We are talking about, of course, Gtk+, WX and Qt.
– To use a more functional or higher-level API. We can easily draw a line between the family of FRP frameworks with GUI backends and other functional frameworks.
Let’s go through them one by one.
Imperative GUI Programming Libraries
Currently in Haskell there are a few binding libraries that allow us to use existing GUI toolkits. The most common are Gtk+ (via gtk2hs), wx (via wxHaskell) and Qt (via qthaskell).
- Gtk+ is, by far, the best supported. It gives a natural interface in all OSs. There is plenty of documentation, and many advanced features are well supported. Installation is easy in all OSs. The results look professional (and we have been using this in production with our clients). On the not-so-bright side, distribution of compiled Gtk+-dependent programs requires carrying a lot of dlls.
- WX is an elegant, cross-platform approach that uses the OSs “native” windowing system. It is not as well-supported as Gtk+: installation, even in Linux systems, is much harder. There is a diagreement between the dependencies of the last version available on hackage and the latests wx library available on debian/ubuntu, making it necessary to download and install dependencies by hand. The obvious solution, reverting back to wxhaskell 0.11, will leave you with a relatively old GUI toolkit. In conclusion: an elegant, clean approach with very few dependencies, but not nearly as well supported as Gtk+.
- QT is, by far, the most elegant and feature-rich of all three. Unfortunately, and to the extent of my knowledge, nobody has been actively working on qthaskell for more than 5 years, so the bindings are pretty outdated. As a result, installation (with a relatively recent version of ghc and cabal) is impossible.
Even when one takes care of writing clean code, these libraries leave a very monadic aftertaste that affects the whole program. Also, and as we will see, this approach has consequences in terms of the application architecture, and we need to take that into consideration if we want our codebase to scale well. Before we delve deeper into those two problems, let’s look at the alternatives.
Writing GUIs with functional libraries
So, let’s imagine that we don’t want to write impertive-looking code, and we want to create some compositional systems (building larger systems in terms of smaller systems). As it turns out, there are a range of more functional alternatives for the pure haskeller. Most of them are based on some form of Functional Reactive Programming, although, at least for the sake of covering other views, we will also present other existing purely functional approaches.
- There are some functional GUI toolkits that represent widgets as functions. That includes, for instance, Fudgets. We may write a post of Fudgets in the future, but there are two main practical issues with Fudgets: 1) it is incredibly outdated, so even if the ideas are sound, nobody has maintained it, and 2) fudgets are widgets with a single type, meaning that each widget has a type (and only one) that it manifests. That last point makes Fudgets not fine-grained enough for realistic purposes.
- Functional Reactive Programming (FRP) has long been regarded as the purely functional way of representing large networks of interdependent visual elements. All variants are based, in one way or another, on the idea of signals, which represent continuously varying values. Because any property can only have one value at a time, it becomes easy to (conceptually) represent signals as functions from time to value. giving us the purely functional approach that we were looking for. Examples always demonstrate a straightforward reading of signal definitions, but realisations have important consequences in terms of efficiency and program architecture, that will be explored below.
Functional Reactive Programming
FRP is often advertised as presenting the following benefits:
- No need to explicitly handle time sampling. One can easily define signals (or transformations between signals) ignoring time altogether. These will later be applied pointwise (or rather, sample-wise).
- No need to explicitly pass on changed values. If a signal depends on another, a change to the value of the latter will provoke a change to the value of the former. (What this really means and how it can be realized, we will see later on).
- Every signal has only one definition. So, for any misbehaving varying element in our application, we need only look in one place to find out where the problem might be.
It’s difficult to argue that these properties are not true. It might be easier, however, to explain why they are not always beneficial, and how, in some applications, the cost of reasoning in terms of Signals and Signal Functions is not worth the effort.
FRP applied to Large Applications
Many programs with GUIs (what we call “applications”) turn out not to have any continuously changing element. Most changes respond to user events (hence the success of event-oriented programming in past years), and many applications do not even have frequently-changing elements.
Some FRP frameworks are built around this idea of event-based programming (eg. reactive-banana), but others (e.g. Yampa) refresh applications either at regular intervals or as frequently as possible. Implementing desktop applications with rarely-changing GUIs using regular or constant refreshes turns out to be an overkill. It might make sense to use Yampa or a similar framework if we really need to paint as frequently as possible, but if the appearance does not change on its own and our input devices are sampled with low frequencies, it might be much better to retort to a more discrete approach.
A second problem with FRP is that, contrary to popular belief, large applications might not benefit so much from such definition-uniqueness property. In larger applications, different changes to the same widgets will correspond to different features. Putting everything in one definition forces us to explicitly name and combine the values in a precise, clear way, which results in increased precision (benefit) and coupled code (disadvantage).
The situation becomes more problematic when we have interdependent elements, since there are a limited number of ways in which we can specify loops in a purely functional sound way. In most cases, we will be forced to give the definitions of interdependent elements together (in the same module and/or function), which only increases coupling.
A final problem inherent to both FRP and Fudgets is that programmers tend to use the widgets’ state to store the information about the problem they are trying to solve, therefore relying on a very simple type system, and forgetting the power that the programming language (be it Haskell, Idris, etc.) gives them to capture the ideas they are trying to address in the most precise manner.
In a future post we will explore how gtk2hs applications can be architectured, the scalability problems that such architectures can have, and what we can do about it.
Please, let your comments and/or request explanations below.