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].
Event
s and output a stream of Command
s. The type of the components is: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.type Dlg state = state -> [Event] -> ([Command], state, [Event])
Sequential composition is defined as
Input and output operations can be 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
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
The type of the sequential composition operator istype Interaction a b = a -> [Event] -> ([Command], b, [Event])
and the definition is the same as forsq :: Interaction a b -> Interaction b c -> Interaction a c
join
above. (Neither
the type of join
nor the type of sq
is the most
general type of this function.) 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:
The bind operation comes with an identity, called>>= :: IO a -> (a -> IO b) -> IO b
return
, which simply returns a value without any I/O.
Thereturn :: a -> IO a
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)
Object
, to capture dynamic evolution of dialogues:
The typedata Object t s = O t (Cond s) ([Object t s] -> Dlg s) type Cond s = s -> [Event] -> Bool
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:
The continuation k can do some I/O, and then again callstreeCase :: [Object t s] -> Dlg t s
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.
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:
The monadic expressionbutton :: Picture -> a -> DC -> IO (Button a, DisplayHandle) label :: String -> DC -> IO (Label, DisplayHandle)
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
:The application handles can be used to modify various aspects of the GUI elements, depending on their type:hbox :: [DisplayHandle] -> DisplayHandle
The most important feature of the button handle is the possibility to wait for it to be clicked:setButtonLabel :: Button a -> Picture -> IO () disableButton :: Button a -> IO () enableButton :: Button a -> IO () setLabel :: Label -> String -> IO ()
WhengetButtonClick :: Button a -> IO a
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 startFigure 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
This combination has the desirable property that a call tocombineButtons :: [Button a] -> IO (Button a)
getButtonClick
waits for any of the push buttons
to be clicked.
In main
, the counter
function is passed to wopen
,
which creates the user interface in a shell window. The first argument towopen :: [String] -> (DC -> IO (a,DisplayHandle)) -> IO (a,Window)
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.
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.
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
and since the typebutton :: [Conf Button] -> Window -> GUI Button
Button
is instance of both HasText
and HasCommand
, its label and callback
function can be configured with the following members:
An up/down counter written with Gofer's do-notation (syntactic sugar for monads) is found in Figure 108.text :: HasText a => String -> Conf a command :: HasCommand a => GUI () -> Conf a
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.
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
is written (roughly)f = do x1 <- c1 x2 <- c2 ... return e
The world-as-value paradigm can be seen as programming in an unfolded variant of thef # (x1,s) = c1 s (x2,s) = c2 s ... = (e,s)
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+dxFigure 109. Up/down counter in Clean.
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.
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.
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.
PizzaButton
and a PizzaLabel
. The PizzaButton
is a Button
where we define the action as a callback
function directly in the constructor:
Theclass PizzaButton extends Button { public PizzaButton(String s, final () -> void action) { super(s); addActionListener( new ActionListener() { public void actionPerformed(ActionEvent e) { action(); } }); } }
PizzaLabel
is a polymorphic Label
with methods
for getting or setting the value, and applying a function to
it.
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.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)); } }
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.
#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:
row
, a layout widget which put all its children in
a row or in a column.display
, which shows a string which will be the count.button
, a button that the user can press. Whenever
this happens, an associated callback routine is called.increment
is registered as a
callback routine for the button widget. increment
increments the counter and updates the display widget.