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: oskar.wickstrom@gmail.com 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 the Exit constructor of Transition .

function transition from the current state, based on an event, to another state. It may also exit the application, using the constructor of . The view function renders the current state as a window widget.

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.

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+!