• Aucun résultat trouvé

Functional Graphical User Interfaces — An Implementation based on GTK

N/A
N/A
Protected

Academic year: 2022

Partager "Functional Graphical User Interfaces — An Implementation based on GTK"

Copied!
19
0
0

Texte intégral

(1)

Functional Graphical User Interfaces — An Implementation based on GTK

Micha l Pa lka June 22, 2007

1 Introduction

Graphical user interfaces (GUIs) as we know them today were created over 20 years ago, yet they are still notoriously difficult to implement. Evan a moder- ately complex interface requires a large amount of code to implement it. A study at Adobe [8] has shown that the amount of GUI event handling code reaches 1/3 in many projects and is a source of one-halve of bugs. This indicates that the popular techniques used in implementing GUIs are rather low-level because they require writing much more code that seems to be necessary.

Despite numerous attempts at changing it, the usual imperative style of programming based on callbacks is still ubiquitous and all mainstream GUI toolkits are based on it. In that scheme, GUI functionality is implemented using callback routines which are triggered by external events, such as button click, and which manually update the program state along with performing required actions. This causes the GUI code responsible for updates to be split into small pieces scattered over multiple callbacks which operate on the same shared state. What emerges is code that that is very unstructured and is often metaphorically described by the term ‘spaghetti code’.

Another problem that contributes to the difficulty of implementing GUIs is the lack ofcompositionalityof code written in the callback style. There is no easy way of connecting two components together as events originating in one of them might require changing the state of the other which can be done by defining the components specifically a priori, or equipping them with complicated callback interfaces.

To conquer these problems, we will present a programming model based on explicit modeling of dataflow diagrams. We assert that this representation is more natural and preserves locality of code much better that the callback style.

Dataflow computation was found to be cleanly expressible in Haskell where combinator libraries provide means of creating domain-specific languages em- bedded in Haskell. Early efforts to employ dataflow representation of GUIs in Haskell include Fudgets [1] developed by Thomas Hallgren and Magnus Carls- son. Dataflow programming has been chosen as a base for Fudgets because it provided a ‘functional feel’ of programming GUIs. Later, the theme was picked up by the FRP group from Yale University who developed Fran [4], a framework for Functional Reactive Programming which was built around the concept of providing manageable denotational semantics to aid reasoning about

(2)

the code. Subsequently the framework was refined to be based on Arrows, a framework for encoding dataflow diagrams, and called YAMPA [7].

In order to model reactive systems that dynamically change their structure, a reactive programming framework must provide means of specifying changes to dataflow diagrams that occur over time. To implement this, the FRP frame- works based on Haskell use a concept calledswitching which means replacing a fragment of the diagram with a new one when a specific event happens. Imple- mentation of switching combinators impose significant constraints on the un- derlying structure of FRP libraries and complicates interaction with imperative GUI libraries.

Fran and YAMPA are both general reactive programming libraries and do not contain GUI toolkits, however they serve as a base of several GUI toolkits, the most notable being Fruit [3,2]. Fruit is an advanced functional GUI toolkit which is developed on top of YAMPA and uses a functional graphics library for drawing widgets. For experts, it supports, among other things, continuous and discrete signals, animation, switching and dynamic collections.

Fruit has been adapted to run on top of an imperative GUI toolkit, wxWid- gets, by Bart Robinson in wxFruit [10] project. Running on top of a mainstream GUI toolkit is beneficial because all the widgets do not have to be implemented from scratch. However, interfacing the imperative style of wxWidgets with the functional style of Fruit raised issues that made it difficult to implement switch- ing combinators properly for widgets and reduced the possibility to use wxFruit for creating dynamic GUIs.

In this report we present a functional GUI toolkit for Haskell similar to wxFruit, however developed from scratch to be based on GTK+ (called GTK later in the report), another mainstream GUI toolkit. The presented toolkit uses GTK for drawing widgets on the screen as well as for laying them out.

Central idea to the toolkit is that its implementation performs actions only in response to external events and remains idle otherwise. Our main contributions are as follows:

• Our implementation uses GTK for drawing widgets without giving up switching combinators; we provide a simple switching combinator. It is implemented using a novel technique based on restricted discrete signal types and double-step evaluation which allows for switching GTK widgets.

• We provide an efficient way of supporting external data models, or Model- View-Controller separation, for complex data models. It is possible, for instance, to have two text views showing the same buffer and populating changes entered into any of them in an effective manner. Earlier func- tional toolkits did support MVC, however no emphasis was placed on this functionality and efficient propagation of changes was impossible.

Our implementation has several limits:

• It does not support animated widgets, since the implementation can wake up only in response to user actions.

• We do not provide advanced switching combinators.

• Layout treatment is limited to hierarchical layout using simple layout com- binators.

(3)

simpleTextEntry String ()

simpleTextOutput () String

Figure 1: Diagrams of components representing text entries

The remainder of the report is structured as follows. Section 2 provides a high-level overview of the toolkit and underlying computation model. Sec- tion 3 presents a more technical description along with a short introduction to Haskell and the Arrows framework useful for understanding the section. Sec- tion 4 presents the selected details from the internals of the toolkit. Section 5 discusses the limitations. Our contributions are located mainly in sections 3.5 and 4.

2 Overview of the toolkit

In this section we will informally introduce the computation model that is the foundation of our toolkit by presenting example GUIs built using it. The model closely resemblessynchronous dataflow programming.

We will be building our GUIs out of components, calledsignal transformers, which we will connect together to form adiagramwhich will also be represented by a signal transformer. Each signal transformer represents a widget or a larger fragment of a GUI and contains input and output ports for exchanging data with other parts of the program. We can interact with a signal transformer only by (1) influencing it by feeding it appropriate input and (2) getting information from it by reading its output. This ensures that the functionality of components is wellencapsulated with input and output ports being the only interface to them.

2.1 Primitive elements

First diagram in Figure1illustrates an example primitive component of a GUI, a text field whose purpose is to hold text entered by user. We indicate that simpleTextEntryoutputs a string of characters by drawing an outgoing arrow labeled withString. Even thoughsimpleTextEntrydoesn’t take any data on input we still add an input port to it for consistency with other components.

In this case input has type () which is a type that has only one value and thus yields no information. Programmers with C background may call it ‘void’

while functional programmers will probably pronounce it ‘unit’. As expected, simpleTextEntryoutputs the text that the widget holds at the moment.

We call input and output ofsimpleTextEntry continuous signals because they yield values at any given moment of execution. However, all continuous signals are in factstep signalssince their values change only on event occurrences and remain constant otherwise.

We will need another simple component to display text computed by an ap- plication. The componentsimpleTextOutputshown in Figure1has a signature dual to that ofsimpleTextEntrythat is it takes aStringand outputs nothing (()). Note thatsimpleTextOutputis suitable for output only and that the user is prevented from editing it directly.

(4)

()

simpleTextEntry simpleTextOutput

() String

Figure 2: Diagram of a simple GUI which accepts a string in the first text field and prints it in the second

Figure 3: Actual GUI from Figure2

2.2 Composite GUIs

We can imagine a very simple GUI that we can build using these two compo- nents. Figure2 shows diagram of a GUI where output of a text entry is fed to another text field. Anything that is entered in the first text field ends up also in the second. The actual window realizing the diagram is shown in Figure3.

This example is a GUI that does no explicit computations on the data; we merely move data around from one widget to another. To process data we introduce a special kind of a signal transformer. For any Haskell function fof typea -> b, which takes a value of type a and returns a value of typeb, our toolkit allows the programmer to build a new component, written arr f, that performs the computations offon its input and delivers the result to its output.

Of coursearr f has an input port of typeaand an output port of typeband it does not exhibit itself as a visible widget.

Once we can express arbitrary computations we are ready to present a more complex example. Here we are going to ask the user to provide her first and last name and show a greeting message to her. The function that will create the message has type(String, String) -> Stringwhich means that it takes a pair ofStrings and returns aString. It can be defined as follows:

makeMessage (firstName, lastName) =

"Hello, " ++ firstName ++ " " ++ lastName ++ "!"

where++is the operator for concatenation of strings. The diagram of the second example is shown in Figure4. It consists of two text entries whose outputs are delivered to the component enclosingmakeMessageand a text field that displays the output of that function. Please note that even though thearr makeMessage component has a single input, we draw two arrows because it accepts a pairs of values on its input. Physically, we will consolidate the two values into a pair before handing them to the component. Resulting GUI is shown in Figure5.

Note on semantics The computation model of signal transformers is com- pletely synchronous, which means that the implementation calculates all needed

(5)

simpleTextEntry String

()

simpleTextEntry

simpleTextOutput

() String

arr makeMessage String ()

Figure 4: Diagram of a simple GUI which accepts two strings and displays a string which is computed from them

Figure 5: example GUI

values in channels before processing next event. For instance, this means that it will wait for the data to reachsimpleTextOutputin Figure2 after a change before dealing with the next change.

2.3 Discrete signals

In the examples we have seen so far, the channels were carrying continuous data, in the sense that the data is being transmitted even in the absence of any related event. For instance, in the example from Figure4, when the user types ”Haskell”, ”Curry” is still transmitted from the ”Last name” widget to the ”Message” widget. On the other hand, we call a signal

In order to model reactions to discrete events like button clicks we need a specific type of signals which will be used to send notifications about the events. Discrete signals will be transmitted over the same channels as normal, continuous values, but will be represented by a datatype Sparse awhich will denote a discrete signal containing a value of type a. A value of type Sparse a will yield no information when there is no event and will contain a value of typea on event occurrence. For instance, in the case ofsimpleButton, which represents an ordinary button, we don’t want any data associated with an event to be transmitted, thus its output port has type ofSparse ().

The datatype for transmitting discrete events, Sparse a, is defined as an opaque datatype, which means that we can not create or analyze its values directly. Instead, the programmer is forced to use special functions which are provided for manipulating them. We will be able to perform two operations on Sparse a: (1) operating on a value carried by an existing discrete signal and (2) merging two discrete signals that are parametrized by the same type. To

(6)

a stepAccum init Sparse (a -> a)

Figure 6: stepAccumsignal transformer

modify a value carried by a discrete signal we will use the following function fmap :: (a -> b) -> Sparse a -> Sparse b1

which takes an operation on values as an argument and applies it on each value that arrives in the signal. In case where the function to be applied is constant we can use a specialized version which replaces the arriving values with a predetermined value

tag :: b -> Sparse a -> Sparse b

If we have two discrete signals carrying the same type of value we can produce a signal which fires each time an event arrives on either of them

merge :: Sparse a -> Sparse a -> Sparse a

Themergeoperator preferes the left value in case of simultaneous occurence.

Restricting manipulation ofSparse ato these functions entails that discrete signals can only be lit as a result of events originating from external sources.

In other words, if a discrete signal is ‘fired’, we know that we are processing an external event. This restriction to the computation model makes it possible for the implementation to wake up only on external events and remain idle otherwise.

Due to the opaque nature of this type we are unable to directly convert Sparse avalues to ordinary, continuous signals. For this we will use a specially provided operation,stepAccumshown in Figure6, which is also the basic oper- ation for creating stateful signal transformers. A signal transformerstepAccum init starts with an initial value initof typeaas its state and updates it ap- plying the function that shows up with each event on input. It also exposes the value of its state continuously on its output effectively making it a stepping operator.

A diagram of an example calculator that uses discrete signals is shown in Figure 7. Discrete signals originate from buttons labeled by digits and opera- tions are tagged by appropriate values of EventDesc, a type which is a union of digits and operations. Next all the tagged signals are merged using multiple applications ofmergedisplayed as a triangle in the diagram. Resulting discrete signal is routed to a component that implements the proper logic of the calcula- tor and is decomposed below. Firstly, a transition function on state is selected by applyingtrans function to a value ofEventDescand the resulting discrete signal is fed to an instance ofstepAccumwhich applies it to the state. The state is constantly consulted by a component which extracts from it a number to be displayed and presents it as aString.

1We read this: fmaphas type(a -> b) -> Sparse a -> Sparse b

(7)

1

+ 9

. . .

0

. . .

=

simpleTextOutput String

Sparse EventDesc

mainLogic

arr (fmap trans) State arr stateToDisp

Sparse EventDesc

stepAccum initState Sparse (State -> State)

String

mainLogic

Figure 7: Simplified diagram of a calculator

2.4 Advanced Idioms

Text entry widgets that were presented so far have simple interfaces that are suitable for input-only or output-only; we are not capable of creating a text entry that a user can edit and whose contents can be altered by the program.

To solve this problem in an elegant fashion we need to introduce some sort of separation between the widget displaying the data and its model, that is the component that holds the data. This approach is commonly called a Model- View-Controller (MVC) separation and identifies three parts: (1) the model, which is a datastructure, (2) a view or multiple views that are widgets displaying the data from the model and (3) the controller, which is responsible for the logic of updates.

We will realize the MVC separation using decomposition shown in Figure8 in whichtextEntryis the view whereas the model and the controller are hidden in textModelcomponent. When the user edits the entry, the event is signaled by sending appropriate value of TextDeltaby the textEntrywhich describes the change made by the user. The controller then decides what to do, typically to apply the delta to the buffer it holds, and sends the updated data2 to its output together with the right delta value.

For example, the controller might decide to capitalize all input done by the user. When ‘abc’ is entered to the text entry, the discrete signal delivers a value of type TextDelta, InsertText pos "abc" (pos denotes position where the text was inserted) to the controller which decides to insert ‘ABC’ instead in the same position. It applies the change to the text buffer and exposes the new buffer on its output along with signaling the change that occurred, that is the insertion of ‘ABC’. Finally, this data reaches the component that displays it.

The component does not have to read the whole updated buffer that arrived on input which might be rather big; alternatively it can make use of the provided delta value and efficiently make local changes.

2Physically, only a reference to data is sent

(8)

TextEntry

Sparse TextDelta textModel

(String, Sparse TextDelta) TextEntry Sparse TextDelta

Figure 8: Two widgets sharing a single model

initial

a (b, Sparse c)

next x

a b

switch initial next Sparse c initial

a

b

Figure 9: Switching combinator

Using this scheme it is possible to easily express a GUI where two text entries share the same model. It is enough to perform merge operation on discrete events produced by the text entries, feed the resulting signal to the model-controller component and route its output to all views. Example of such GUI is shown in Figure8. Note that this design involves no redundant state as only the model holds the data.

2.5 Dynamic interfaces

Interfaces can change the configuration of widgets by using a simple switching combinatorswitch. Theswitchcombinator is a GUI component that accepts two arguments: a component that will be the initial configuration and a function which takes a single argument and returns a component that will replace the initial one. Figure9shows the switch combinator together with diagrams of its arguments: a component initialand a function nexttaking an argument of typec.

In the beginning,switch initial nextbehaves like its first argument with the exception that it outputs only the first component of pair. Second compo- nent is examined byswitchwhich waits until an event occurs on it. On event, the value carried by it is passed to next function that produces a component that will replace the whole switch.

3 Technical overview

In this section we will describe our toolkit in more concrete terms. It will start with a quick primer to Haskell and then we will proceed to explaining how to construct primitive and composite GUIs.

(9)

3.1 Brief introduction to Haskell

Haskell is a non-strict statically-typed purely-functional programming language.

Being a purely-functional language means that computations and side-effects are clearly separated. All computations take form of evaluation of expressions whereas all side-effects are modeled explicitly, for example as a list of actions to do, or are encapsulated in control structures such as monads.

Haskell programs consist of definitions written in a form of equations. Let us recall a definition from earlier in the report which will serve as an example.

makeMessage (firstName, lastName) =

"Hello, " ++ firstName ++ " " ++ lastName ++ "!"

It defines a function that takes one argument, a pair of Strings, and returns a message that contains both of them. Note that even though Haskell is statically typed we didn’t have to declare the type of makeMessage. Instead, the type is inferred by the compiler to be(String, String) -> Stringwhich denotes a type of functions from a pair ofStrings to aString. The type can be guessed by the compiler becauseStringis the only type that can be used this way. We will be writing typing judgments in following way:

makeMessage :: (String, String) -> String

which is pronounced makeMessagehas type(String, String) -> String.

Evaluation of functions in Haskell is performed differently than in most other languages. Instead of calling a function after evaluating all of its arguments, the arguments are passed unevaluated and their values computed only if needed.

This strategy is called non-strict evaluation and is a very advantageous feature.

Among other things, it aids embedding domain-specific languages in Haskell.

Fundamental way of representing data in Haskell is by creating algebraic datatypes. Example definition looks like this

data Bool = False | True

which defines a datatype with two values given by constructorsTrueandFalse.

We can also create polymorphic datatypes, that is datatypes which depend on their type parameters.

data Pair a b = Pair a b

In this contextPairis the name of the datatype andaandbare type variables which range over all types. On the right-hand side of equality sign we have defined one constructor Pair a b which holds one value of each parameter type. Therefore, a value of type Pair Int String, for example, holds one integer and oneString. Type Pair a bis isomorphic to(a, b)which is the standard Haskell type for representing pairs.

As mentioned earlier, computations in Haskell are completely separated from side-effects. The separation is accomplished by encapsulating all side effects in a special type, IO a which represents a computation possibly involving side- effects and yielding a result of type a. For example, if we wanted to create a function fromatobthat yields side-effects, it would be of typea -> IO b.

(10)

arr f b a

v b

a c

v >>> w v

v b a

first v c

Figure 10: Diagrams of basic operations on arrows

3.2 Signal transformers

We will be representing our GUI components as signal transformers each of which has typeSignal a b. Informally, a value ofSignal a bdenotes a signal function which transforms values of typeainto values of type b. For example, we can imagine a signal function

incr :: Signal Int Int

which takes an integer and outputs its successor. We will also use values of type Signal a b to represent widgets visible on the screen. For example, a signal transformer representing a text entry as in Figure1 has type

simpleTextEntry :: Signal () String

which indicates that it doesn’t take any data from other computations and that it outputs a string that has been entered to it.

We should stress the difference between ‘pure’ signal transformers and ‘rich’

signal transformers. Whereasincris a signal transformer that acts as a regular, pure function whose output is fully determined by the input,simpleTextEntry, on the other hand, is not pure because its output does not only depend on its input, which is constant, and also because it exhibits itself as a visible widget.

The typeSignal a b provides a very simple interface to widgets. The only thing that we can do with a widget is to connect its input to a source of data and collect its output. Therefore, we view a widget as a black box with input and output channels.

3.3 Composing signal transformers

Once we have primitive building blocks for an interface we can start putting them together. For this we will be using theArrowsframework which provides a systematic way of composing elements and connecting their inputs and outputs.

Arrows were introduced to Haskell by John Hughes [5] and later refined by Ross Paterson [9] who added theloopcombinator for encoding diagrams with cycles and developed a special notation to aid writing clear programs using arrows.

To use these facilities we need to implement three fundamental operations:

arr :: (a -> b) -> Signal a b

(>>>) :: Signal a b -> Signal b c -> arrow a c first :: Signal a b -> Signal (a, c) (b, c)

Diagrams of the operations are shown in Figure10. First operation,arr, lifts a pure function to a signal transformer, that is, produces an arrow that does ex- actly the same thing as the lifted function. We call resulting signal transformers

(11)

a c

b

d

e

a b c d e

Figure 11: Encoding of acyclic graphs using arrows

v b a

loop v c

Figure 12: Theloopcombinator

pure, because they do nothing except pure calculations to compute the result.

The other two operations are for composing arrows. Serial composition oper- ator (>>>) connects output of the first transformer to the input of the second.

Operation firstcreates a transformer which behaves like its argument in the first component of pair and passes through second component unchanged.

It turns out that those three operations are sufficient to encode any acyclic graph of widgets. Example of such encoding is explained in Figure 11 where diagram on top is encoded into an expression shown again as a diagram on the bottom. The primitive components are arranged in the order of topological sort so that all edges are oriented from left to right. Dotted rectangles indicate applications offirstwhich are used to bypass primitive components if the edge is directed to a component which occurs later on in the sequence. All of them are joined by the sequential composition operator (>>>) with applications ofarr plumbX inserted between them where plumbX are functions that redirect data to correct places. For instance, the one located between first bandfirst c swaps two components of a pair.

plumb2 ~(x, y) = (y, x)

Please note that the encoding is not bijective as one graph may be represented in various ways. Full explanation of the technique is presented in [9].

We are also interested in creating programs where interconnections between components form cycles. For this to be possible to express using arrows we need only one more operation

loop :: Signal (a, c) (b, c) -> Signal a b

which takes a signal transformer acting on pairs and creates a loop on the second component of the pair as shown in Figure 12. To encode a diagram

(12)

with cycles we employ a similar technique as described before, but we enclose whole expression loop which will deliver backward edges to the beginning of the sequence from where they can be routed by the basic technique.

As a simple example we show the code for composing the widgets of Figure 8.

rec d1 <- textEntry -< s d2 <- textEntry -< s

s <- textModel "" -< d1 ‘merge‘ d2 returnA -< ()

3.4 Layout combinators

Composition using arrow operations imposes a linear order on the components which we exploit for specifying layout. We will present two basic layout combi- nators which provide essential hierarchical layout capabilities. The combinators have following signatures

vbox :: Signal a b -> Signal a b hbox :: Signal a b -> Signal a b

and are responsible for vertical and horizontal layout. Applying a layout com- binator to a specific fragment of a GUI specifies relative layout of components of that GUI and is completely transparent otherwise. A component is under influence of the closest enclosing layout operator which makes creating complex hierarchical layouts a matter of nested applications of the combinators.

3.5 Model-view-controller revisited

In Section2.4we showed a text entry and an external data model that cooperate by sending change notifications back and forth between each other. Specifically, the text entry was being notified of changes in the model by a signal of type (String, Sparse TextDelta). The view has access to the whole text buffer given in the first component, but is also allowed to use the discrete signal. It is used for advertising changes to avoid doing unnecessary work when the data doesn’t change and to apply changes that arrive with notifications instead of reading the whole buffer. However, there are two things that could go wrong with this representation:

• The programmer could replace one of the components of the pair to some other value violating the assertion that the second component describes the change of the first; or

• he could redirect the output of another text model to the entry at some point and, because of lack of the event on the discrete signal indicating changes, the view would not notice the change and not update its display.

To address these problems, we present an opaque datatype which will be used in the place of the pair(a, Sparse d).

data CompoundData a d

where typeadenotes data and typedchanges. The simplest thing that we can do withCompoundData a dis to extract the data from it.

(13)

extractData :: CompoundData a d -> a

However, if we wanted to benefit from change notification we should use another operation implemented as a signal transformer:

data CompoundAction a d = SwapContents a | ApplyDelta d getCompoundAction :: Signal (CompoundData a d)

(Sparse (CompoundAction a d)) This operation exists to protect a view from wrong interpretations of changes to data. If the source of the data suddenly changes, the operation detects this and outputs an instructionSwapContents which orders the view to completely replace its data. However, in presence of a valid update ApplyDelta is gen- erated. This behaviour is implemented using special markers contained in the CompoundDatadatatype.

4 Implementation

This section describes briefly the internals of the toolkit and how it interacts with the GTK toolkit. Parts of this section that provide intuitive descriptions are intermixed with more technical ones.

4.1 Overview of GTK

Before we discuss the implementation, we should outline the style which is used for programming applications based on GTK. GTK is a multi-platform GUI toolkit implemented as a C library which provides a typical imperative programming interface. Programs written on top of GTK areevent-driven and their behavior is implemented in callbacks. A callback is a function that is being executed by the toolkit code in response to some event. For instance, if the example from Figure 3 was implemented directly in GTK, when the user would enter something into the first text entry, a callback associated with the

’text-changed’ event of that entry would be launched. This would execute code which would extract the contents of the entry and copy it into the second entry.

Typically, control reaches the code written by the programmer of the application for a very brief moment and is returned immediately to the toolkit code either by invoking its functions or by returning from the callback.

To combine a library written in Haskell with one written in C special wrap- ping code is needed. This functionality is provided by the Gtk2Hs library which is a wrapper-library that allows for developing programs using GTK in Haskell.

Being a wrapper-library means that Gtk2Hs strictly mirrors the API of GTK in Haskell without adding any high-level abstractions in functional style and that programming using it follows the same imperative style of GTK. A substantial part of our work consists of adjusting the implementation of our functional GUI to cooperate with the imperative approach of GTK.

4.2 Basic version

For simplicity, we will start by discussing a basic version of signal transformer which does not support all the features. Conceptually, one calculation step executed on a transformer consists of three phases:

(14)

extract

post action

a b

Figure 13: Graphical sketch of a signal transformer

arr f empty post-action empty extractor

v w

joined post actions v >>> w

v

first v

Figure 14: Arrow operations interfering with extractors and post-actions

1. Extraction of values from the underlying toolkit;

2. Calculation making use of the extracted values from phase 1 and of the explicit input from other parts of the diagram;

3. Execution of post-actions generated by phase 2, which affect the underly- ing toolkit.

Graphical sketch of a signal transformer from a to b is shown in Figure 13.

The internal representation include extraction and generating post-action (dot- ted arrows) but the interface hides them leaving only input and output chan- nels exposed (solid arrows) to the programmer. For example in the case of simpleTextEntrythe three phases are as follows:

1. Extraction of text that is held in the widget;

2. Calculation ignores the explicit (void) input and presents the extracted text as its output;

3. Post-action is empty.

Signal transformers which are composite are executed exactly in the same way as simple ones. In this case the extracted values are routed independently to all contained simple transformers, and post-actions are sequenced together to form a composite post-action. Composition of signal transformers with respect to extractors and post-actions is shown in Figure 14 (recall that any diagram can be represented using three primitive operationsarr,>>>andfirst).

4.2.1 Technical Details

Below we show the type of the function which is used to create basic signal trans- formers represented by a simpler datatype SignalEval a b. Missing features will be added later be means of an arrow transformer.

(15)

simpleSignalEval :: (IO a’) -- extraction -> (a -> a’ -> (b, IO ()))

-- calculation yielding -- value and post-action -> SignalEval a b

Using the function forces the right style of programming signal transformers where each calculation step consists of (1) extracting the needed values from underlying imperative toolkit yielding values of typea’and (2) performing pure computation that has access to extracted values and input of the transformer (a) which produces the output value (b) and an action (IO ()) which will be run just after the step. Extraction and computation are then together represented as a single function of typea -> IO (b, IO ())which is hidden in the datatype SignalEval a b.

data SignalEval a b = SignalEval (a -> IO (b, IO ()))

Composition of signal transformers yields a value of typeSignalEval a b with all extracting actions embedded inside and producing final value in addition to a post-action consisting of a sequence of post-actions of all its elements. Code executing one step of the computation is structured as follows:

runSignalEval :: SignalEval a b -> a -> IO b runSignalEval (SignalEval v) x = do

(y, postAction) <- v x -- execute proper calculation postAction -- execute post-action

Being able to execute computation steps in response to events delivered by the underlying GUI toolkit is essential for integrating an imperative GUI toolkit with a functional interface. To realize it we connect each event that requires taking action to a global callback that launchesrunSignalEval. Subsequently, runSignalEvalautomatically extracts all the needed values, performs calcula- tions and updates the GUI by executing post-actions.

This procedure, however, must be augmented if a signal transformer is a source of a discrete signal. We would like the extraction phase of such signal transformer to yield a flag saying whether the signal should be marked active or not. Since there is no way to ask GTK if a widget is the origin of the event we need to add special logic to each signal transformer that will keep track of whether the event occurred on current execution step or not. This special logic is implemented in a local callback function that is registered to the widget at its creation. Instead of directly triggering the global callback it changes the value of a specially-kept reference to indicate the signal before calling the global callback.

This reference will then be read in the extraction phase and will eventually get reset to its default value after the execution of the global callback.

4.3 Dynamic features

To represent mutable state enclosed within GUI components an enriched version of the main datatype must be used. Each computation step produces a new version of the structure in addition to ordinary output as shown in Figure 15.

After a step the signal transformer is replaced by its new version, that could be called itscontinuation, and which reflects the change in its state. The interface

(16)

step n

step n + 1

...

Figure 15: Stateful signal transformer

visible to the programmer remains the same with the additional output hidden.

As the simplest example, all stateless signal transformers transmit their own copies as their continuations.

Passing continuations also allows for a direct implementation of switching of pure signal transformers because we can simply pass the desired new signal transformer as the continuation. Switching of widgets is more problematic as we need to explicitly register and unregister them in the layout hierarchy. We will, however, omit this detail from our description.

4.3.1 Double-step implementation

The presence of switching combinators implies that a computation step may result in a complete change of a fragment of a GUI. As a consequence, our library may need to issue commands displaying the new widgets immediately after completing the calculation and before a new event arrives. However, displaying some widgets requires knowing the inputs of their signal transformers which in effect forces us to recalculate the whole diagram. Therefore the implementation performs two calculation steps in response to each event: one with the source discrete signal ’turned on’ and one with all discrete signals blank.

The second calculation step is there to populate all widgets with the right data and cannot cause another switch to occur because no discrete signal could be lit thanks to the restrictions posed on manipulatingSparse avalues. We as- sert that this additional fake step does not change the semantics of our interface, although we do not provide a formal proof for it.

4.3.2 Details of dynamic implementation

To express producing continuations by signal transformers we will use an arrow transformer from the Arrow Template Library

data Automaton a b c = Automaton (a b (c, Automaton a b c)) This transformer enriches an arrow type a by creating an arrow type which has the same input (b) but whose output is augmented by a continuation ((c, Automaton a b c)). Therefore our datatype will have a form of

type Signal a b = Automaton SignalEval a b

(17)

To signal transformers producing continuations, the global callback handling function must be adapted to use a global reference holding latest version of the GUI which is update after each double-step.

4.4 Implementation of layout and other details

Implementing hierarchical layout requires augmenting the main datatype even more. We add one more value to the hidden output of each signal transformer which holds the information about widgets contained inside and can is used by enclosing widgets to actually add the widgets to their lists of children.

We do not provide combinators for specifying dynamic layout because it may be specified directly using a simple combination of the switching combinator from Section2.5and of ordinary layout combinators.

5 Discussion of implementation

During the implementation of our toolkit we made the decision to use arrow transformers instead of a single arrow type in hope that this would lead to cleaner code. However, it is not obvious if this decision was right. On one hand using arrow transformers allows to hide some features of an arrow type from places where it is not used leading to simpler code. On the other hand, the design of our arrow is rather complex and there are cases where we had to ‘cross boundaries’ of arrow transformers using features of all of them at once which led to ugly results. Furthermore, should we one day decide to implement dynamic optimizations, as outlined in [6], we would be forced to convert to monolithic arrow type.

The performance of the library seems satisfactory, although no serious bench- marks have been conducted. Small-scale examples exhibited no noticeable per- formance disadvantage when compared to pure GTK programs.

The most obvious limitation of the library, which also affects all known libraries based on the dataflow model, is the fact that the dataflow diagram must be recomputed completely on each step. This leads to O(n) complexity, with n being the total number of GUI components, even in case of minor updates.

Dynamic optimization techniques known to be implemented for Fruit attempt to alleviate this burden, however they seem to be just shaving off the constant factors instead of reducing the complexity.

The toolkit supports only one simple version of switching combinator which is intended to serve as a proof of concept, but is not enough to handle all situa- tions. Implementing other types of switching combinators is likely possible using the current design, however it has not been made because of time constraints imposed by the project.

6 Conclusions and future work

This work has shown that it is feasible to implement a functional GUI toolkit based on dataflow model on top of an imperative one without losing switching features. Only a limited switching combinator has been implemented to date, however we believe that there are no fundamental obstacles against implement- ing the general switching combinators.

(18)

Furthermore, we have shown how to express MVC separation in dataflow model elegantly and efficiently by explicitly transmitting the changes to the data between views and the controller.

The presented toolkit is far from being a viable alternative to mainstream GUI toolkits; it is of ‘research’ quality and supports only a very limited set of widgets, yet it looks as a prospective candidate for further development.

Apart from adding the support for the remaining GTK widgets and imple- menting extra switching combinators, there are other areas for improvement.

It would be beneficial to implement custom layout engine instead of relying on the one provided by GTK which would make controlling layout easier. It would be also possible to experiment with relaxing the hierarchical layout model.

However, custom layout engine would probably require bypassing usual GTK rendering and using some low-level primitives, possibly GTK-engine API.

Optimizing the implementation is another broad topic. Two obvious pos- sibilities include static optimization using GHC rewrite rules and dynamic op- timization using specially-crafted datatype that explicitly models special cases that are possible to simplify [6].

References

[1] Magnus Carlsson and Thomas Hallgren. Fudgets - Purely Functional Pro- cesses with applications to Graphical User Interfaces. PhD thesis, Chalmers University of Technology, Gteborg University, 1998.

[2] Antony Courtney.Modeling User Interfaces in a Functional Language. PhD thesis, Yale University, March 2004.

[3] Antony Courtney and Conal Elliott. Genuinely functional user interfaces.

InHaskell Workshop 01, 2001.

[4] Conal Elliott and Paul Hudak. Functional reactive animation. Proceed- ings of the second ACM SIGPLAN international conference on Functional programming, pages 263–273, 1997.

[5] John Hughes. Generalising monads to arrows. Science of Computer Pro- gramming, 37(1-3):67–111, 2000.

[6] Henrik Nilsson. Dynamic optimization for functional reactive program- ming using generalized algebraic data types. In Proceedings of the Tenth ACM SIGPLAN International Conference on Functional Programming (ICFP’05), pages 54–65, Tallinn, Estonia, September 2005. ACM Press.

[7] Henrik Nilsson, Antony Courtney, and John Peterson. Functional reac- tive programming, continued. InProceedings of the 2002 ACM SIGPLAN Haskell Workshop (Haskell’02), pages 51–64, Pittsburgh, Pennsylvania, USA, October 2002. ACM Press.

[8] Sean Parent. A Possible Future of Software Development. Keynote presen- tation at Library-Centric Software Design Workshop, 2006. Slides available athttp://opensource.adobe.com/wiki/images/0/0c/Possible future.pdf.

(19)

[9] Ross Paterson. A new notation for arrows. Proceedings of the sixth ACM SIGPLAN international conference on Functional programming, pages 229–

240, 2001.

[10] Bart Robinson. wxFruit: A Practical GUI Toolkit for Functional Reactive Programming, March 2004.

Références

Documents relatifs

In this paper, we proposed an analysis of digital graphic design tools to better understand the current mismatch between designers and their tools. We first showed how design

L’archive ouverte pluridisciplinaire HAL, est destinée au dépôt et à la diffusion de documents scientifiques de niveau recherche, publiés ou non, émanant des

Being in need of such a tool to browse and query a dataset on comic books, described by an ontology [10], we propose an efficient Java implementation for the calculation of

The fact that the optimal value for τ and the relative average errors vary very little between the art style and wars experiments show that the method can be used to extract periods

Ap´ ery of the irrationality of ζ(3) in 1976, a number of articles have been devoted to the study of Diophantine properties of values of the Riemann zeta function at positive

A.nother approach to user interfaci! design is by way of using visual programming languages based on the hypothesis that two-dimensional visual languages are easier to learn

Gtk-fortran (gtk-fortran team, 2011–2018) is a GTK+ / Fortran binding using the ISO_C_BINDING intrinsic module for interoperability between C and Fortran defined since the Fortran

Being in need of such a tool to browse and query a dataset on comic books, described by an ontology [8], we propose an efficient Java implementation for the calculation of the