41 Related work

We start by giving a brief overview of combinators for sequential I/O in Section 41.1. Section 41.2 discusses stream processing and combinations of concurrency with functional programming. Section 41.3 presents other GUI toolkits written in functional languages, and Section 41.4 presents some functional GUI libraries written on top of imperative toolkits. Section 41.5 discusses toolkits which are not GUI toolkits in the traditional sense, but can be used to write interactive programs with (animated) graphics. Finally, Section 41.6 presents two imperative GUI toolkits.

41.1 Combinators for sequential I/O

As noted in Chapter 4, the stream I/O model allows us to write interactive programs in a pure, lazy functional language. The model does not impose any specific way of composing subprograms into larger programs.

Sequential composition is useful for structuring textual user interfaces, where the interaction can be seen as a dialogue between the computer and the user, that is, a linear sequence of input and output actions. In the following we give a brief overview of combinators for sequential composition of effects. More developed reviews can be found in Noble's and Gordon's theses [Nob95][Gor92].

41.1.1 Dialogues

The dialogue combinators by O'Donnell [O'D85] allow stream I/O programs being built from components using sequential composition, and were used to build a programming environment. Programs are assumed to input a stream of Events and output a stream of Commands. The type of the components is:
type Dlg state = state -> [Event] -> ([Command], state, [Event])
The idea is that a component consumes an initial segment of the input stream and returns some commands to be output and the remainder of the input stream. It may also use and modify some global state information.

Sequential composition is defined as

join :: Dlg state -> Dlg state -> Dlg state
join dlg1 dlg2 state1 events1 = (cmds1++cmds2,state3,events3)
  where
    (cmds1,state2,events2) = dlg1 state1 events1
    (cmds2,state3,events3) = dlg2 state2 events2
Input and output operations can be defined as:

put :: Command -> Dlg state
put cmd state events = ([cmd],state,events)

get :: (Event -> Dlg state) -> Dlg state
get edlg state (event:events) = edlg state event events

41.1.2 Interactions

A refinement of the dialogue combinators is Thompson's interactions [Tho90]. The idea is much the same, but instead of manipulating a global state, interactions input a value of some type and output a value of another type:

type Interaction a b = a -> [Event] -> ([Command], b, [Event])
The type of the sequential composition operator is

sq :: Interaction a b -> Interaction b c -> Interaction a c
and the definition is the same as for join above. (Neither the type of join nor the type of sq is the most general type of this function.)

41.1.3 Monads

Monads provide an even more general approach to I/O, and have also been used for process programming, something we will see in later sections. Monads were first a vehicle for giving denotational semantics for imperative programming languages [Mog91], but the concept was then carried over to practical use [Wad95][PJW93]. The same kind of structure had then already been used in the KAOS project as a refinement of Thompson's interactions [Tur87,Cup89], and by Gordon [Gor89].

Monads for I/O build on a type IO a--which represents I/O effects that return a value of type a, when carried out--and the bind operation >>=, which is used for sequential composition. The bind operation also binds the return value of the first I/O operation to a variable so that it can be used in further operations in the sequence:

>>= :: IO a -> (a -> IO b) -> IO b
The bind operation comes with an identity, called return, which simply returns a value without any I/O.

return :: a -> IO a
The IO type can be seen as a function that transforms the world regarded as a state, and also returns a value:

type IO a = WorldState -> (a,WorldState)

41.3 Functional GUI toolkits

There are a number of GUI toolkits written in functional languages which implement widget sets on top of X Windows. In the following, we review Gadgets, Haggis, BriX and eXene, but first we want to mention an early example of functional GUI programming by Dwelly, although it was not a presented as a GUI toolkit [Dwe89]. Dwelly's work was based on the dialogue combinators, with the addition of a recursive type Object, to capture dynamic evolution of dialogues:

data Object t s  = O t (Cond s) ([Object t s] -> Dlg s)
type Cond s      = s -> [Event] -> Bool
The type Object t s represents a potential dialogue. An object value O t c k has a tag t, and a condition predicate c, which signals if the continuation dialogue k is applicable in the current state of the program. Among other things, the conditions predicates were used to test if the user had clicked within the area that a button occupied. If the predicate c is true, k is applied to a list of active objects to get a dialogue. This is done by the function treeCase, which takes a list of active objects as an argument, and schedules the first object with a true condition predicate:

treeCase :: [Object t s] -> Dlg t s
The continuation k can do some I/O, and then again calls treeCase, with a manipulated list of active objects, thus allowing a new set of possible dialogues. In the manipulation, the tags are used as pointers into the list.

41.3.1 Gadgets

Noble has implemented a GUI library called Gadget Gofer [Nob95], where Gadget stands for generalised fudget. The motivation for this name is that gadgets are processes that communicate via typed, asynchronous channels (called wires), thus allowing a gadget to have an arbitrary number of input and output ``pins''. As a proof that gadgets are more general than fudgets, Noble implemented the basic fudget combinators using gadgets. (For an implementation of Gadgets in Fudgets, see Chapter 31.)

Noble implemented process scheduling and channel communication in the runtime system of Gofer [Jon91], and added primitives for communication with X Windows. A feature of Gadgets is that it only uses the most basic drawing operations in the Xlib interface in one single X window. On top of this, Noble has implemented a functional window system, complete with a window manager.

The gadget in Figure 106 implements the up/down counter, except that it uses a bar graph to display the value.

main = go [(counter,"Up/Down")]

counter :: Gadget
counter =
    wire $ \w ->
    let b1 = button' (picture uparrow) (op w) (+1)
        b2 = button' (picture downarrow) (op w) (+(-1))
        g = bargraph [ip w] in
    wrap' (border 20) (b1 <|> g <|> b2)

Figure 106. The Gadget up/down counter.

The counter gadget uses the following library gadgets:

button'   :: Change ButtonAttributes -> Out a -> a -> Gadget
bargraph  :: [In (Int -> Int)] -> Gadget
wrap'     :: Change WrapAttributes -> Gadget -> Gadget
Gadgets uses the same mechanism for default parameters as described in Chapter 15, so button' and wrap' are customisable versions of button and wrap. The button gadget button o a will send the value a on the wire output end o, whenever it is clicked. The gadget bargraph is waits for input functions on any of the wire input ends in is, and when such a function arrives, it is used to update the level of the bar graph. Note how the wire w is used to connect the two buttons to the bar graph. The layout of the three gadgets is specified to be vertical using the operator <|>. The example shows how the specifications of layout (by gadget combinators) and dataflow (by wires) are separated in Gadgets. Finally, the wrap' gadget puts some space around the three gadgets.

The button parameter picture is used to specify up and down arrows:

uparrow, downarrow :: DrawFun
The type DrawFun roughly corresponds to the FlexibleDrawings in Section 27.3.

41.3.2 Haggis

Just like Gadgets, Haggis [FP96] is based on a process extension of a functional language, namely Concurrent Haskell.

The separation between user interface and application code can be explained by studying the type of a couple of common GUI element, namely push buttons and labels:

button  :: Picture -> a  -> DC -> IO (Button a,  DisplayHandle)
label   :: String        -> DC -> IO (Label,     DisplayHandle)
The monadic expression button p v d creates a button which will show the picture p. The button's value is v, and d is an environment, or display context which carries default values (Haggis uses this for customisation, instead of the default parameter mechanism in Fudgets and Gadgets). The monadic expression returns an application handle of type Button, and a display handle. The GUI element label does also return a display handle, but its application handle has a different type. The display handles are pointers to the GUI elements, and can be combined with other display handles with layout combinators, for example hbox:
hbox :: [DisplayHandle] -> DisplayHandle
The application handles can be used to modify various aspects of the GUI elements, depending on their type:
setButtonLabel  :: Button a -> Picture -> IO ()
disableButton   :: Button a -> IO ()
enableButton    :: Button a -> IO ()

setLabel        :: Label -> String -> IO ()
The most important feature of the button handle is the possibility to wait for it to be clicked:
getButtonClick :: Button a -> IO a
When getButtonClick b is called in a process, it will be suspended until the user clicks b, and then the button's value is returned. Internally, this uses a trigger (which can be seen as value carrying condition variable), one of several synchronisation abstractions that Haggis provides on top of Concurrent Haskell's value carrying semaphore type MVar.

The type Picture corresponds somewhat to the Drawing type in Section 27.4, and permits advanced structured graphics to be defined. Haggis pictures are described further in [FJ95].

In Figure 107, we see a version of the the up/down counter in Haggis.

counter :: DC -> IO ((Label, Button (Int->Int)), DisplayHandle)
counter env =
   label (show start)            env  >>= \(lab,ldh) ->
   button (text "Up") (+1)       env  >>= \(inc,idh) ->
   button (text "Down") (+(-1))  env  >>= \(dec,ddh) ->
   combineButtons [inc,dec]           >>= \btn ->
   return ((lab,btn), hbox [ldh, idh, ddh])

start = 0

main =
   wopen ["*name: Counter"] counter >>= \((lab,btn),_) ->

   let count n = getButtonClick btn      >>= \f ->
                 let n' = f n in
                 setLabel lab (show n')  >>
                 count n'
   in
   count start

Figure 107. The Haggis up/down counter.

The function counter defines the user interface. It returns a display handle, and handles to the label and a combination of the two buttons, created by

combineButtons :: [Button a] -> IO (Button a)
This combination has the desirable property that a call to getButtonClick waits for any of the push buttons to be clicked.

In main, the counter function is passed to wopen,

wopen :: [String] -> (DC -> IO (a,DisplayHandle)) 
                  -> IO (a,Window)
which creates the user interface in a shell window. The first argument to wopen can contain default values for the display context. The example indicates that the format for these values are similar to the resource data base in X [SG86]. In the example, it is used to set the window title. The application handles in counter are returned as they are from wopen, which also returns a window handle which can be used to manipulate the shell window.

The rest of main defines the application behaviour of the program by defining a loop which waits for button clicks, and then updates the label. In this example, the loop comes right after the initialisation of the interface in the main process, but in general, control loops are spawned as separate processes.

41.3.3 BriX

The toolkit BriX [Ser95] is built on top of X11 as part of the Bristol Haskell System [HDD95], which aims at building concurrent and distributed systems in a strictly deterministic manner. BriX inherits this deterministic view of the world, and indeterministic merge is avoided by propagating information about events through parallel compositions. This has similarities with the synthetic oracles used in an early version of the stream processors (Section 20.4.1).

41.3.4 eXene

The toolkit eXene, by Reppy and Gansner [RG91,GR91], is an X Windows toolkit written in a strict functional language, namely Standard ML of New Jersey [SML]. It is written on top of Concurrent ML (CML) [Rep91a], and is thus multi-threaded. eXene pushes the functional border further: even Xlib is thrown out, and the communication with X is written in ML.

Events from the X server and control messages between widgets are distributed in streams (coded as CML event values) through the window hierarchy, where each window has at least one CML thread taking care of the events. Drawing is done by calling imperative drawing procedures. High-level events are reported either imperatively or by message passing: when a button is pressed, a callback routine is called, or a message is output on a CML channel.

41.4 Interfaces to existing toolkits

A number of interfaces for functional languages have been built on top of existing imperative toolkits. Early examples include Lazy Wafe by Sinclair [Sin92], XView/Miranda by Singh [Sin91] and MIRAX by Tebbs [Teb91]. More recent examples are Taylor's Embracing Windows (using Hugs and Windows 95), and TkGofer [VTS95]. The latter offers a monadic interface in Gofer to the popular toolkit Tk [Ous94]. Application programs are written using a combination of functional abstractions and a traditional imperative style with callbacks that mutate variables or modify widgets.

TkGofer was further developed and improved in [CVM97], by using Gofer's expressive class system to provide a typed means of specifying parameters for the widgets, similar to the dynamically customisable fudgets in Section 30.3. The result is that most dynamic aspects of the Tk widgets can be controlled in a type-safe way. For example, the button widget has type

button :: [Conf Button] -> Window -> GUI Button
and since the type Button is instance of both HasText and HasCommand, its label and callback function can be configured with the following members:

text :: HasText a => String -> Conf a
command :: HasCommand a => GUI () -> Conf a
An up/down counter written with Gofer's do-notation (syntactic sugar for monads) is found in Figure 108.
counter :: IO ()
counter = start $
  do w <- window [title "Up/Down Counter"]
     e <- entry  [initValue 0, readOnly True] w

     let my_button t f = button [text t, 
                                 command (modifyEntry e f)] w
     u <- my_button "Up" (+1)
     d <- my_button "Down" (+(-1))
     pack (u ^-^ e ^-^ d)

modifyEntry :: Entry Int -> (Int -> Int) -> GUI ()
modifyEntry e f =
  do x <- getValue e
     setValue e (f x)

Figure 108. The TkGofer counter.

41.4.1 Concurrent Clean

Concurrent Clean is an efficient implementation of a lazy functional language, which was originally developed for Macintosh [Pv96]. It comes with an I/O library which permits portable development of GUI programs that interface to the GUI toolkits on Macintosh, Windows'95/NT and XView or OpenLook.

I/O in Clean is carried out using the world-as-value paradigm [Ach96], which means that an abstract value, representing the state of the world (or parts of it), is passed around as an extra parameter in the program. The type system is extended with a mechanism to guarantee that the world parameters are passed in a single-threaded way throughout the program. It is this parameter threading that specifies the order in which I/O operations are performed; no explicit sequencing combinator is used in the world-as-value style. However, Clean programs have a syntactic abbreviation for nested let expressions, which is used when specifying sequences statements. Using this style, the monadic definition

f = 
  do x1 <- c1
     x2 <- c2
     ...
     return e
is written (roughly)

f
   #  (x1,s) = c1 s
      (x2,s) = c2 s
      ...
   =  (e,s)
The world-as-value paradigm can be seen as programming in an unfolded variant of the IO monad in Section 41.1.3. A disadvantage is that state and error handling becomes explicit, something which clutters the programs. On the other hand, different kinds of state parameters can be handled--possibly simultaneously--without the need of defining new combinators.

A Clean version of the up/down counter is shown in Figure 109. The first lines in initcounter show the use of the nested-let sugar, and allocate unique identifiers to be used in the data structure dialog, which specifies the GUI. This data structure also relates the callback function upd to the push buttons, and the initial local state.

::  NoState = NoState

Start :: *World -> *World
Start world = startIO NoState NoState [initcounter] [] world
where
    initcounter ps
      # (windowid, ps)    = accPIO openId ps
        (displayid, ps)   = accPIO openId ps
        (_,ps)            = openDialog NoState (dialog windowid displayid) ps
      = ps
      where
        dialog windowId displayId
            = Dialog "Counter" 
                {   newLS   = init
                ,   newDef  =     EditControl (toString init) dwidth dheight 
                                      [   ControlPos          (Center,zero)
                                      ,   ControlId           displayId
                                      ,   ControlSelectState  Unable
                                      ]
                            :+:   ButtonControl "-" 
                                      [   ControlPos          (Center,zero)
                                      ,   ControlFunction     (upd (-1))
                                      ]
                            :+:   ButtonControl "+"
                                      [   ControlFunction     (upd 1)
                                      ]
                }
                [   WindowClose (noLS closeProcess)
                ,   WindowId windowId
                ]
          where
            dwidth    = 200
            dheight   = 1
            init      = 0

            upd :: Int (Int,PSt .l .p) -> (Int,PSt .l .p)
            upd dx (n,ps) =
                (n1,appPIO (setWindow
                              windowId 
                              [setControlTexts [(displayId,toString n1)]]) ps)
              where n1 = n+dx

Figure 109. Up/down counter in Clean.

41.5 Functional interactive graphics

41.5.1 Pidgets

The idea behind Pidgets, by Enno Scholz [Sch96], is to combine pictures with widgets, to allow arbitrarily shaped objects to be sensitive to input and to change dynamically. Definitions of pictures and some auxiliary types of values, for example, numbers, vectors and colors, can refer to mutable variables. When a variable is changed, and a picture that depends on it is visible in a window, the window is automatically updated.

Pictures are described in the PostScript model [Ado90] for graphics. A picture can made sensitive to input by associating it with a handler. The handler is called if an input event, such as a mouse button press, occurs while the mouse pointer is over the screen area covered by the picture. The handler returns a value of type IO () and can thus have arbitrary I/O effects, including changing a mutable variable that the picture depends on.

Pidgets is based on an imperative approach to dynamically changing graphical objects. Monads are used to provide a purely functional interface to the imperative machinery. Mutable variables are made part of the I/O monad. A new monad Expr is defined for expressions (that is, values whose interdependencies are described by a directed acyclic graph) that can depend on the values of mutable variables.

In part, the purpose of Pidgets is similar to that of the fudget graphicsF discussed in Chapter 27. An interesting experiment would be to see how Pidgets could be used to implement combinators for syntax directed editors.

41.5.2 Fran

Fran (Functional Reactive Animation) by Elliott and Hudak [Ell97] is a Haskell library which supports a declarative specification of 2D and 3D animation, as well as sound. The basic datatypes in Fran are behaviours and events. Behaviours can be viewed as values that vary with time, which is continuous. A behaviour value that specifies a picture is the basic animation mechanism. Events can be external (for example, a button press), or calculated (for example, two objects that collide), and are associated with the time at which they occur.

The reactivity is achieved by combinators that allow a behaviour to be replaced by another at the occurrence of an event. There are also combinators for building complex behaviours and events from simpler ones. The behaviour combinators can be seen as parallel composition of processes, allowing a number of behaviours to act concurrently.

The primary goal for Fran is to specify multimedia and animation, which it does in an elegant and declarative way. It might be possible to use Fran for building complete GUI toolkits as well.

41.6 Imperative toolkits

41.6.1 Java

In the object-oriented programming language Java [GJS96], graphical user interfaces can be programmed using the class library AWT (Abstract Window Toolkit) [AWT]. Figure 110 shows how the up/down counter is defined as a subclass of the Frame, which is used to construct top-level windows. The constructor method UpDown creates two button objects and a label object, and adds so called action listeners (high-level event handlers) to the buttons, as anonymous classes. These play the role of callbacks, and modify the counter variable and the display.

The last lines in the constructor method defines the layout and adds the buttons to the frame.

public class UpDown extends Frame {

    private int count = 0;

    public UpDown() {

      final Label display = new Label();
      display.setText(""+count);

      Button up = new Button("Up");
      up.addActionListener(
        new ActionListener() {
          public void actionPerformed(ActionEvent e) {
            display.setText(""+ ++count);
          }  
        });

      Button down = new Button("Down");
      down.addActionListener(
        new ActionListener() {
          public void actionPerformed(ActionEvent e) {
            display.setText(""+ --count);
          }
        });

      setLayout(new FlowLayout());
      add(up);
      add(display);
      add(down);
    }

  public static void main(String args[]) {
    UpDown a = new UpDown();
    a.setTitle("Up/Down Counter");
    a.pack();
    a.show();
  }
}

Figure 110. Up/down counter in Java.

41.6.2 Pizza

The Java extension Pizza [OW97] allows the programmer to write polymorphic code and use first-class functions. Of course, the AWT library can be used directly in Pizza, but the Pizza programmer may also use a style which more resembles functional/imperative toolkits like TkGofer, using callback functions instead of classes. We exemplify this by defining a PizzaButton and a PizzaLabel. The PizzaButton is a Button where we define the action as a callback function directly in the constructor:

class PizzaButton extends Button {
  public PizzaButton(String s, final () -> void action) {
    super(s);
    addActionListener(
      new ActionListener() {
        public void actionPerformed(ActionEvent e) { 
          action(); 
        }
    });
  }
}
The PizzaLabel is a polymorphic Label with methods for getting or setting the value, and applying a function to it.

class PizzaLabel<T> extends Label {
  private T value;

  public PizzaLabel(T i)          {  super("" + i);
                                     value = i;        }

  public T get()                  {  return value;     }

  public void set(T i)            {  value = i;
                                     setText("" + i);  }

  public void modify((T) -> T f)  {  set(f(value));    }
}
The Pizza up/down counter in Figure 111 is almost the same as the Java counter, except that it does not use a local variable, and uses callbacks instead of action listeners for the buttons.
public class UpDown extends Frame {

    public UpDown() {
      PizzaLabel<int> display = new PizzaLabel(0);

      Button up = 
        new PizzaButton("Up",
                        fun() -> void {
                        display.modify(fun(int x)->int {
                                       return x+1;
                                       });
                        });

      Button down = 
        new PizzaButton("Down",
                        fun() -> void {
                        display.modify(fun(int x)->int {
                                       return x-1;
                                       }); 
                        });

      setLayout(new FlowLayout());
      add(up);
      add(display);
      add(down);
    }

  public static void main(String args[]) {
    UpDown a = new UpDown();
    a.setTitle("Up/Down Counter");
    a.pack();
    a.show();
  }
}

Figure 111. The Pizza up/down counter.

41.6.3 C and Motif

For C-programmers, the toolkit Motif [young:motif] has been a popular choice. An implementation of the counter example in C using Motif is shown in Figure 112.
#include <stdio.h>
#include <X11/Intrinsic.h>
#include <X11/StringDefs.h>
#include <Xm/Xm.h>
#include <Xm/Label.h>
#include <Xm/PushB.h>
#include <Xm/RowColumn.h>

static int count = 0;

static void SetDisplay(Widget display, int i)
{
  char s[10];
  Arg wargs[1];
  int n = 0;

  sprintf(s, "%d", i);
  XtSetArg(wargs[n], XmNlabelString,
           XmStringCreate(s, XmSTRING_DEFAULT_CHARSET)); n++;
  XtSetValues(display, wargs, n);
}

static void increment(Widget b, Widget display, XtPointer call_data)
{
  count++;
  SetDisplay(display, count);
}

int main(int argc, char *argv[])
{
  Widget top, row, display, button;

  top = XtInitialize("counter", "Counter", NULL, 0, &argc, argv);
  row =    XtCreateManagedWidget("row", xmRowColumnWidgetClass,
                                  top, NULL, 0);
  display= XtCreateManagedWidget("display", xmLabelWidgetClass,
                                  row, NULL, 0);
  button = XtCreateManagedWidget("button", xmPushButtonWidgetClass,
                                  row, NULL, 0);
  SetDisplay(display, count);
  XtAddCallback(button, XmNactivateCallback, 
                (XtCallbackProc)increment, (XtPointer)display);
  XtRealizeWidget(top);

  XtMainLoop(); /* does not return */
}

Figure 112. The up/down counter in C and Motif.

The program starts with creating a shell widget called top, which will be the root of the widget tree. The rest of the tree is created with repeated calls of XtCreateManagedWidget, where the arguments specify what kind of widget to create, and where to put it in the tree. The widgets are:

When the widget tree is created, the display is reset to show zero, and the C-function increment is registered as a callback routine for the button widget. increment increments the counter and updates the display widget.