Purely Functional GTK+, Part 1: Hello World
In the last episode we explored gi-gtk, a package providing Haskell bindings to the GTK+ library, and noted in the end that the programming style was imperative and object-oriented. In this episode, we’ll program in a more functional style using gi-gtk-declarative.
Our goal goal is to build a to-do list application, in the style of TodoMVC, using GTK+ and a functional programming style. The application will not have all the features included in TodoMVC, but hopefully enough to demonstrate how gi-gtk-declarative
is used.
We’re starting out with a Cabal file containing a stanza for the todo-gtk
application.
cabal-version: 2.2
name: purely-functional-gtk
version: 0.1.0.0
license: MPL-2.0
license-file: LICENSE
author: Oskar Wickström
maintainer: [email protected]
extra-source-files: CHANGELOG.md
executable todo-gtk
build-depends: base >= 4.12 && < 5
hs-source-dirs: src
main-is: Main.hs
default-language: Haskell2010
ghc-options: -Wall
It’s missing a few dependencies, so let’s begin by adding those. In addition to the gi-gtk
bindings, we add gi-gtk-declarative
and the gi-gtk-declarative-app-simple
packages. We’ll also use text
and vector
.
executable todo-gtk
build-depends: base >= 4.12 && < 5
, gi-gtk
, gi-gtk-declarative
, gi-gtk-declarative-app-simple
, text
, vector
hs-source-dirs: src
main-is: Main.hs
default-language: Haskell2010
ghc-options: -Wall
Finally, the declarative GTK+ packages use threading, which requires the executable to be linked with the threaded runtime of GHC. We add the -threaded
flag to the GHC options of todo-gtk
.
executable todo-gtk
build-depends: base >= 4.12 && < 5
, gi-gtk
, gi-gtk-declarative
, gi-gtk-declarative-app-simple
, text
, vector
hs-source-dirs: src
main-is: Main.hs
default-language: Haskell2010
ghc-options: -Wall -threaded
OK, our Cabal file is in good shape. Let’s head over to the Main
module and start building the application!
Getting Started
Before we start thinking about to-do lists, let’s get a “Hello, World!” example up and running. We need to import the GTK+ bindings from gi-gtk
, and we do so with an import qualified as Gtk
. This module contains all the widget types, signals, and functions, that we need from GTK+. We also import GI.Gtk.Declarative
and GI.Gtk.Declarative.App.Simple
.
import qualified GI.Gtk as Gtk
import GI.Gtk.Declarative
import GI.Gtk.Declarative.App.Simple
GI.Gtk.Declarative
defines the layer that extends GI.Gtk
with declarative capabilities. The App.Simple module provides an application architecture based on a state reducer, inspired by the Pux framework in PureScript, and earlier versions of the Elm architecture.
When using the App.Simple framework, we need a state type and an event type. We use the unit type for state. Our Event
data type has a single constructor called Closed
, which we’ll emit when the application window is somehow closed by the user.
type State = ()
data Event = Closed
In the main
action, we construct an App
and use run
to start the application loop. The App
type is provided by the App.Simple framework, and requires us to define a few things:
- The
update
function transition from the current state, based on an event, to another state. It may also exit the application, using theExit
constructor ofTransition
. - The
view
function renders the current state as a window widget. inputs
is a list of Pipes producers, emitting events that feed into the state reducer loop. This is useful to emit events at regular intervals or to plug in external sources.- The
initialState
is the state value that we begin with.
We’ll need two functions, view'
and update'
that we’ll define shortly. The inputs list is empty, and the initial state is ()
, the only possible value.
main :: IO ()
main = run App {view = view', update = update', inputs = [], initialState = ()}
To get this to type-check, we define the view'
and update'
functions at the top level using typed holes. By doing so, we pin down the specific state and event types of the application to run.
view' :: State -> AppView Gtk.Window Event
view' = _
update' :: State -> Event -> Transition State Event
update' = _
OK! Let’s begin by defining the view function. We ignore the state, which is always the ()
value. The top level widget must be some type of window, and we’ll use the regular Window
widget in GTK+.
view' :: State -> AppView Gtk.Window Event
view' _ = bin Gtk.Window [#title := "Demo"]
In the object-oriented style we’d create the window using new
, and the type of such an action would be IO Gtk.Window
. In the declarative style, we instead use a function called bin
, taking a widget constructor, a list of attributes, and a child widget. This is a pure function, returning a value of type Widget Event
.
Just as in the previous screencast covering gi-gtk
, we use the OverloadedLabels
, OverloadedStrings
, and OverloadedLists
language extensions.
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
module Main where
We leave a typed hole for the child widget.
view' :: State -> AppView Gtk.Window Event
view' _ = bin Gtk.Window [#title := "Demo"] _
First, we’re going to add an event handler to the window’s list of attributes. We want to connect it to the delete-event signal, which is emitted when the window is closed. When that happens, we want our program to terminate. The on
function takes a signal label and the event handler.
The exact type of the event handler depends on the signal, as GTK+ callbacks have different type signatures with varying number of arguments and return types. You can read more about these types in the gi-gtk-declarative documentation.
In the case of delete-event, the event handler type will be a function from the underlying GDK window event to some return type. Let’s use a typed hole to find out what type it is!
view' :: State -> AppView Gtk.Window Event
view' s = bin
Gtk.Window
[ #title := "Demo"
, on #deleteEvent (const _)
]
_
GHC tells us it’s (Bool, Event)
. The Bool
value is used as the return value of the underlying GTK+ callback, which decides if the event should be stopped from propagating to other handlers. The Event
value is the one emitted by our declarative widget.
We stop the event from propagating, and emit a Closed
event.
view' :: State -> AppView Gtk.Window Event
view' s = bin
Gtk.Window
[ #title := "Demo"
, on #deleteEvent (const (True, Closed))
]
_
Next, let’s fill the child widget hole. We declare a Gtk.Label
widget using the widget
function. This is similar to bin
, but it doesn’t accept any child widget.
view' :: State -> AppView Gtk.Window Event
view' s = bin
Gtk.Window
[ #title := "Demo"
, on #deleteEvent (const (True, Closed))
]
$ widget Gtk.Label [#label := "Hello, World!"]
Our view function is done. Let’s implement the update function! We don’t care about the unit state value, and we only have one possible event. When the Closed
event is emitted, we exit the application.
update' :: State -> Event -> Transition State Event
update' _ Closed = Exit
That’s it! We can now run our application. When we close the window, the program exits.
Stay tuned for the next part of Purely Functional GTK+!