While a lot of the work I tend to do in Haskell is building libraries, there have been plenty of times where I would have loved to provide a GUI. Usually, I shy away from this task, and settle for a command line interface - working with GUI libraries is usually a nightmare, both in code and deployment! In July earlier this year, Heinrich Apfelmus announced the release of his new GUI library threepenny-gui
, which can solve both of these problems. threepenny-gui
is a Haskell library to build GUIs, but with a twist: rather than deal with bindings to GTK or QT, threepenny-gui
uses an interface that is already almost universally acceptable - web pages!
threepenny-gui
is thus a collection of common UI elements, a way of composing them into web pages, and the ability to watch these interface elements for interactions. threepenny-gui
also uses web sockets to provide a tight feedback loop between the client and the server. Today, we’ll explore the threepenny-gui
library by building a to-do list.
Before we even begin considering how to write the GUI, we should start by defining a few data types that will sufficiently model the application. We could use persistent
for our data store, as we at saw yesterday - but instead I’ll just work with an in-memory database using Haskell types.
type Database = Map UserName ToDoList
type UserName = String
type ToDoList = Set String
Our “database” will be a map from usernames to to-do lists, where a username is just a String
and a to-do list is a Set
of String
s - very straight forward. Next, we can start up a server for our application using threepenny-gui
:
main :: IO ()
= startGUI defaultConfig setup
main
setup :: Window -> UI ()
= undefined setup rootWindow
startGUI
begins a HTTP server, here we use the default configuration which listens on port 10000. The setup
action takes a Window
- which corresponds to the browser page the client is viewing - and builds up the UI of the entire application. Every client connection will call setup
on connection, which means by default there is no cross-communication or state. To add state and cross-communication, we need to explicitly introduce a shared variable. To achieve this sharing, we’ll use a mutable variable within the STM framework:
main :: IO ()
= do
main <- atomically $ newTVar (Map.empty)
database
startGUI defaultConfig (setup database)
setup :: TVar Database -> Window -> UI ()
= do setup database rootWindow
Now we’re in a position to start building our UI. The first stage of the UI is to prompt the client for their username in order to determine which to-do list to display to them:
setup :: TVar Database -> Window -> UI ()
= void $ do
setup database rootWindow <- UI.input
userNameInput # set (attr "placeholder") "User name"
<- UI.button #+ [ string "Login" ]
loginButton
#+
getBody rootWindow map element [ userNameInput, loginButton ]
First of all we call UI.input
, which creates a new <input>
element. The #
operator is reverse function application, which we use to then change the placeholder attribute of our newly created text field. To create our login button, we create a button element with UI.button
, and then we add a string element as a child of the button element in order to set the text content.
Now that we have both of these UI elements, we can append them to the body of the rootWindow
UI element. getBody
takes a Window
and returns the corresponding Element
for the window, which we can append to using #+
.
Next, we need to respond to the user clicking the “login” button, which we do by observing the click
event:
$ \_ -> do on UI.click loginButton
Now, what do we need to do when someone clicks “login”? We need to look up that username in our database and, if they exist, present a list of to-do items the user has created. We should also show an input field to add new to-do items. The first part is straight forward:
<- get value userNameInput
userName
<- fmap Set.toList $ liftIO $ atomically $ do
currentItems <- readTVar database
db case Map.lookup userName db of
Nothing -> do
writeTVar database (Map.insert userName Set.empty db)return Set.empty
Just items -> return items
We get the value of the userNameInput
input field, and then enter an STM transaction to find all the to-do items. We attempt to lookup the user in the database, and if they have items we return them. Otherwise, we “register” a new user and give them an empty set of to-do items. Next, we render the user’s to-do items:
let showItem item = UI.li #+ [ string item ]
<- UI.ul #+ map showItem currentItems toDoContainer
Again, straight forward! I just map a showItem
routine over each to-do entry, and contain them all in a <ul>
container. We’ll need to add new entries to this list later, so we’ll bind the <ul>
element to a name.
The last bit of functionality is the ability to create new to-do items. For this, lets use another <input>
element, and when the user presses “return” have the item be added to the list:
<- UI.input
newItem
$ \input -> do
on UI.sendValue newItem $ atomically $ modifyTVar database $
liftIO
Map.adjust (Set.insert input) userName
"" (element newItem)
set UI.value #+ [ showItem input ] element toDoContainer
A new input element is created, and we listen for the sendValue
event, which is produced when a client presses return. Then we run a little STM transaction to add a new item to the to-do list. Once this transaction completes, we clear the input field and append the newly created to-do item to the to-do list.
Finally, we combine this all together into a UI:
<- UI.h1 #+ [ string $ userName ++ "'s To-Do List" ]
header
set children
[ header, toDoContainer, newItem ] (getBody rootWindow)
And we’re done! If we head over to http://localhost:10000
we’re presented with a username field, and if we fill this in we’ll be shown a list of to-do items. Changing the values and then logging in as the same user again shows the items have persisted, just as we were intending.
Today is my first exposure to threepenny-gui
, and overall I really like what I’m seeing! In a little over 50 lines of code we have a functioning application, and I feel I understand a lot of what I’m doing. There are a few parts that still feel a little odd - for example passing around UI Element
s, where (to me) it would feel more natural to just pass an Element
instead. Nonetheless, that’s a minor concern that shouldn’t stop you experimenting with this fantastic project.
I also started my first draft of this post raving on about FRP, but observant readers will notice we didn’t really use any of the FRP functionality that threepenny-gui
offers. Sadly, I didn’t get time to touch this part of the library, and I think it has the potential to do more interesting things. For example, making the to-do list collaborative by reacting to asynchronous changes to the to-do list.
As always, the code for today’s example is over on Github - feel free to have a play and see what you come up with!
You can contact me via email at ollie@ocharles.org.uk or tweet to me @acid2. I share almost all of my work at GitHub. This post is licensed under a Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported License.