|
USENIX 2002 Annual Technical Conference, Freenix Track - Paper   
[USENIX 2002 Technical Program Index]
Biglook: a Widget Library for the Scheme Programming Language
Abstract: IntroductionWe have studied the problem of constructing Gui in functional languages by designing a widget library for the Scheme programming language, called Biglook. In this paper we focus on how to apply the functional style to Guis programming.Biglook's primary use is to implement graphical applications (e.g., xload , editors à la Emacs, browser
à la Netscape, programming tools such as kbrowse ,
our graphical Scheme code browser). Figure 1 presents
two screen shots of Biglook applications: i) kbrowse on the
left and ii) the Biglook dock applications, à la
NextStep, on the right. These Biglook applications are used on a daily
basis.By contrast to previous work, no attempt has been made to make that library familiar to programmers used to imperative or purely object oriented programming style. On the contrary, our library introduces an original application programming interface (Api) that benefits from the high level constructions of the extended Scheme implementation named Bigloo [25] which is open source and freely available since 1992. The main Bigloo component is an optimizing compiler that delivers small and efficient applications for the Unixtm operating system. Bigloo is able to produce native code (via C) and JVM bytecode. Currently Biglook uses GTK+ [20] associated with the Bigloo C back-end and Swing [29] with the Bigloo JVM back-end. The previous release of Biglook [12] used Tk [19]. Bigloo implements an object layer inspired by Clos [1]. It is a class-based model where methods override generic functions and are not declared inside classes as in Smalltalk [13], O'Caml [22] or Java [14]. Biglook is implemented as a wrapping layer on top of native widget libraries (that we name henceforth the back-end). This software architecture saves the effort of implementing low-level constructions (pixel switching, clipping, event handling and so on) allowing to focus on the Scheme implementation of new features. When designing the Biglook Api, we always had to decide which model to choose: the functional model or the object model. We think that these two models are not contradictory but complementary. For instance, if the widget hierarchy naturally fits a class hierarchy, user call-backs are naturally implemented by the means of Scheme closures. In Section 1 we briefly present the Bigloo system emphasizing its module system and its object layer. This section is required for readers unfamiliar with the Clos model and it also serves as an introduction to virtual slots. Virtual slots are a new Bigloo construction that is required in order to separate the Biglook Api made of classes and the native back-end. They are presented in Section 2. In Section 3 we present the Biglook library. We start showing a simple Biglook application and its associated source code. Then, we detail the Biglook programming principles (widgets creation, event handling, etc.) motivating our design orientations by programming language considerations. In this section we are conducted to compare the functional programming style and the object oriented one. In Section 4 we present the Biglook implementation. At last, in Section 5 we present a comparison with related work. 1 BiglooBigloo is an open implementation of the Scheme programming language. From the beginning, the aim was to propose a realistic and pragmatic alternative to the strict Scheme [17]. Bigloo does not implement ``all'' of Scheme; for example, the execution of tail-recursion may allocate memory. On the other hand, Bigloo implements numerous extensions: support for lexical and syntactic analysis, pattern matching, an exception mechanism, a foreign interface, an object layer and a module language. In this Section, we present Bigloo's modules and its object model; that is, class declarations and generic functions. Virtual slots, which are heavily used in Biglook, are presented in Section 2.1.1 ModulesBigloo modules have two basic purposes: one is to allow separate compilation and the second is to increase the number of errors that can be detected by the compiler. Bigloo modules are simple and they have been designed with the concern of an easy implementation.A module is a compilation unit for Bigloo. It is represented by one or more files and has the following syntax:
Import clauses are used to import bindings in the module. In order to import, one just needs to state the identifier to be imported and its source module. Note that a shorthand exists to import all the bindings of a module. Export and static clauses play a close role. They point out to the compiler that the module implements some bindings and distinguish those that can be used within other modules (they are exported) and those that cannot (they are static). These clauses do not contain identifiers but prototypes. It is then possible to export variables (mutable bindings) or functions (read-only bindings). Static clauses are optional (the bindings of a module which are not referenced in a clause are, by default, static). Library clauses enable programs to use Bigloo libraries. A Bigloo library is merely a collection of pre-compiled modules. Using a library is equivalent to importing all the modules composing the library. Detailed information on Bigloo modules may be found in the two papers [25, 26]. 1.2 Object layerIn this paper we assume a Clos-like object model with single inheritance and single dispatch. The object layer implemented in Bigloo is a restricted version of Clos [1] inspired, to a great extent, by Meroon [21].1.2.1 Class declarationsClasses can be declared static or exported. It is then possible to make a declaration accessible from another module or to limit its scope to one module. The abbreviated syntax of a class declaration is:
A class can inherit from a single super class. Classes with no specified super class inherit from the object class. The type
associated with a subclass is a subtype of the type of the super
class.A class may be provided with an initialization function (opt-init) that is automatically called each time an instance of class-id is created. Initialization functions accept one argument, the created instance. A slot may be typed (with the annotation ::type-id) and may have a default value ( default
option). Here are some possible declarations for the traditional
point and point-3d :
1.2.2 InstancesWhen declaring a class cla, Bigloo automatically generates the predicatecla? , an allocator
instantiate:: cla, an instance cloner
duplicate :: cla, accessors (e.g., cla-x for a
slot x ), modifiers (e.g., cla-x-set! for a slot x ) and an
abbreviated special access form, with-access:: cla, to
allow accessing and writing slots simply by using their
name. Here is how to allocate and access an instance of
point-3d:
The instantiate and with-access special forms are implemented by
the means of macros that statically resolve the keyword
parameters (such as x , y and z ). For instance, the above example is
expanded into:
As we can see, since macros are expanded at compile-time, there is no run-time penalty associated with keyword parameters. 1.2.3 Generic functions and methodsGeneric function declarations are function declarations annotated by thegeneric keyword. They can be exported, which means that they can
be used from other modules and that methods can be added to those
functions from other modules. They can also be static, that is, not
accessible from within other modules, hich means that no other modules
can add methods. The Clos model fits harmoniously with the traditional
functional programming style because a function can be thought as a
generic function overridden with exactly one method. The syntax to
define a generic function is similar to an ordinary function
definition:
Generic functions must have at least one argument as this will be used to solve the dynamic dispatch of methods. This argument is of a type T and it is impossible to override generic functions with methods whose first argument is not of a subtype of T. Methods are declared by the following syntactic form:
Methods override generic function definitions. When a generic function is called, the most specific applicable method, that is the method defined for the closest dynamic type of the instance, is dynamically selected. A method may explicitly invoke the next most specific method overriding the generic function definition for one of its super classes (a class has only one direct super class but several indirect super classes) by the means of the (call-next-method)
form. It calls the method that should have been used if the current
method had not been defined.Here is an example of a generic function that illustrates the use of the Bigloo object layer. We are presenting a function that prints the value of the slots of the point and point-3d instances. This
generic function is named show :
Then, the generic function is overridden with a method for classes point
and point-3d .
Hereafter is an example of a call to the show generic function.
2 Virtual slotsBigloo supports two kind of instance slots: regular slots that have already been described in Section 1.2.1, and virtual slots that enable several views of a single data. As we will see in Section 4.2, virtual slots are at the heart of the Biglook implementation. They are mandatory to present Biglook to the user as a class based Api. In particular, wrapping native widgets (such as GTK+ or Swing) for the Bigloo object model requires virtual slots.Using virtual slots gives the illusion of accessing the slots of a class instance but instead, Scheme functions are called. As we have seen in Section 1.2.2, the compiler automatically defines getters and setters that access the various values embedded in the instances regular slots. Accessing virtual slots is syntactically identical to accessing plain slots, but virtual slots differ in the following way:
rect is characterized by its
origin (x0 , y0 ) and either its upper right point (x1 , y1 ) or
its dimension (width , height ). In the following class definition,
the width and height slots are virtual.
Setting the width virtual slot (resp. the height slot)
automatically adjusts the x1 value (resp. the y1 value) and vice versa. No memory is allocated for width and height , as
their values are computed each time they are accessed.3 The Biglook libraryBiglook is an Object Oriented Scheme library for constructing Guis. It offers an extensive set of widgets such as labels, buttons, text editors, gauges, canvases. Most of the functionality it offers are available through classes rather than through ad-hoc functions. For instance, instead of having the classical functionsiconify , deiconify and window-iconified? for the
window widget, Biglook offers the virtual slot visible to
implement this functionality. Setting the visible slot enables
iconification/deiconification. Reading the visible slot unveils the
window iconification state. As we will see, this design choice
yields a simpler Api and allows usage of introspective techniques
for Guis programming.In Section 3.1 we first present a small interface and discuss how to create the widgets which compose it in Section 3.2. Section 3.4 describes the notion of container widget and placement rules. Finally, in Section 3.5 we show how to make a widget reactive to an external event such as a mouse click. Throughout this section we justify the choices we have made when designing the Biglook Api. Our reflection on how to create a widget or how to handle interfaces events are presented in Sections 3.3 and 3.6. 3.1 A Biglook exampleBiglook uses a declarative model for constructing Guis. This permits a clear separation between the code of the interface and the code of the application. The construction of an interface starts by declaring the various widgets which compose it. Then, the behavior of each widget is specified independently of its creation by associating an action (a Scheme closure) with a widget specific event (key pressed, mouse click, mouse motion, ...). Figure 2 is a screen shot of a simple Biglook application. It is made of a window (here named ``A Biglook example''), a check button (the toggle button underline), a radio button group (bold, italic, plain), and a plain button (Quit). The source code of this example is given Figure 3. From that code, we see that in order to access Biglook classes and functions, the program uses a library module clause line 1. This program creates a window (line 4), and three widgets (line 7, line 11 & 17). In the sections 3.2 and 3.4 we present how to create widgets and how to place them in a window. The Figure 2 interface is inert, that is, no action is associated with the widgets yet. We will see in Section 3.5 how actions can be associated with widgets. 3.2 Widget CreationThe graphical objects (i.e., widgets) defined by the Biglook library such as menus, labels or buttons are represented by Bigloo classes. Each class defines a set of slots that implement the configuration of the instances. Consequently, tuning the look of a widget consists in assigning correct values to its slots. The library offers standard default values for each widget but these values can of course be changed. Generally the customization is done at widget creation time. For instance, the radio group of Figure 3 will be displayed horizontally and with a border size of 2 pixels (lines 13 and 14). A particular aspect of a widget can be changed by setting a new value to its corresponding slot. For instance, the expression
changes the orientation of the radio group forcing a re-display of the whole window. Of course, the value of this slot can be retrieved by just reading it:
Remember that, as seen in Section 1.2.2, instead of using the functions created by Bigloo that fetch and write the value of a slot, one may write an equivalent program using the Bigloo special form with-access :
In the rest of this paper, we will use either forms for accessing class slots. 3.3 Reflection on widget creationBiglook widget creation supports variable number of arguments and keyword parameters (parameters that can be passed in any order because their actual value is associated with a name). We have found these features very useful in order to enable declarative programming for Gui applications. For instance, a plain Biglook button is characterized by 20 slots. Some of them describe the graphical representation (colors, border sizes, ...), some others describe the internal state of the button (widget parent, associated value, associated text, ...). In general, these numerous slots have default values. When an instance is created, only slots that have no default values must be provided. Slots are initialized with their default value unless a user value is specified. As a consequence, the form that operates widget construction (e.g., class instantiation) must accept a variable number of arguments. Only some slot values must be provided, others are optional. In addition, because widget constructors accept a large number of parameters, it is convenient to name them and to be able to pass them in any order. This is made possible in Biglook by theinstantiate::
form. As this form is implemented using macros that are expanded into
calls to the class constructors where each declared slot is provided
with a value, there is no run-time overhead associated with forms
such as instantiate:: .Lacking variable number of arguments or keywords disables declarative programming style for Guis because widgets have to be created and, in a second step, specific attributes have to be provided. Even overloading and class constructors do not help. Let's suppose our window classes implemented in Java Awt [15] or Swing [29]. To enable a full declarative style, we should provide the button class definition with 220 constructors (a constructor for each possible combination of provided slots). Even for much smaller classes, this is impractical because, in general, overloading dispatches on types only and several slots can have the same type. For instance, imagine that we want to change the graphical appearance of our window. Instead of using the smallest area large enough to display the three widgets, we want to force the width of the window to a specific value. We can turn the definition of Figure 3, line 4 to:
If we want to specify both width and height, we can use:
Languages relying on type overloading cannot propose these different constructors because the width and the height of a window are of the same type. Languages without overloading nor n-ary functions traditionally use lists to collect optional and keyworded arguments. In addition to the runtime cost imposed by the list constructions, the called function has to dispatch, at runtime, over the list to set the parameters values. Furthermore, such a call cannot be statically typed anymore. 3.4 Containers and widget placementBiglook uses special sort of widgets to enable user customized widget placements: thecontainer class. A container is a widget that can
embed other widgets. Those widgets are called the children of
the container. For instance, a window such as the one
defined line 4 of Figure 3,
is a container, it ``contains'' the three other widgets. With the exception
of windows, all
widgets must be associated with a container in order to be visible on
the screen. To associate a widget with a container, one have to set its
parent slot (see lines 8, 12 & 18 of
Figure 3). Biglook proposes several kind of
containers: aligned containers (such as boxes and windows), grid
containers, note pad containers, paned containers, containers with
scrollbars, etc.Let us present here two examples of containers. First, let us consider that we want to modify the interface of Figure 2. We want the buttons to be displayed horizontally instead of vertically (see Figure 4). For that, we add a new container in the window, an horizontal box :
For the second example, we combine containers to design complex interfaces. For instance, the interface of Figure 5 can be implemented as:
Note that even if the interfaces of Figures 2 and 5 seem quite different, we only need to modify the parent slot of the acheck and aradio widgets to embed them
in the new containers atab (line 5), apane
(line 8) and ascroll (line 12).3.5 Event ManagementBiglook widgets allow the creation of complex Guis with minimal efforts. In general, when building such interfaces, one of the main difficulties lies in trying to separate the code of the interface from the rest of the program. Making the Gui code independent from the rest of the application is important because:
event
slot. This slot must contain an instance of the class
event-handler which is defined as:
Each slot of an event-handler is a procedure called a call-back that accepts one argument. When a graphical action occurs on a widget, the associated call-back is invoked passing it an event descriptor. Those descriptors are allocated by the Biglook runtime system. They are instances of the event class
which is defined as:
So, modifying the example of Figure 3 for the Quit button to be aware of mouse button 1 clicks, we could write the code as follows:
That is, on line 4 we allocate evt , an
instance of the event handler class. That event handler is connected
to the button line 7. The event handler only reacts
to mouse press events. When such an event is raised, the
call-back line 1 is invoked, its formal parameter e
being bound to an instance of the event class. This function checks the
button number of the raised event (line 2). When the first
button is pressed the Biglook application exits. It is possible to modify already connected call-backs. For instance, if we want the Quit button to emit a sound when it is pressed. we can write:
Since widgets call-backs are plain Scheme closures, they can be manipulated as first class objects, as in this example where new call-backs capture the values of the old call-backs in order to reuse them. 3.6 Reflection on event handlingMost modern widget toolkits (with the exception of Qt [5]) use a call-back framework. That is, user commands are associated with specific events (such as mouse click, mouse motion, keyboard inputs, ...). When an event is raised, the user command is invoked. We think that the closure mechanism is the most simple and efficient way to implement call-backs even if some alternatives exist.The rest of this section discusses how call-backs can be implemented depending on the features provided by the host language used to implement a graphical toolkit. 3.6.1 Languages that support functions without environmentISO-C [16] supports global functions but no local functions. A C function is always top-level and may only access its parameters and the set of global variables. C functions have no definition environment. However, without an environment, a call-back is extremely restricted. In particular, a call-back is likely to access the widget that owns it. In GTK+ (a C toolkit) when a call-back is associated with an event, an optional value may be specified that will be passed to the call-back when the event is raised. This user value actually is the environment of the call-back. GTK+ mimics closures with its explicit parameter-passing scheme. We may notice that the allocation and the management of the closure environment is in charge of the client application.3.6.2 Languages with classesFor languages with classes such as Java, another strategy can be used. Call-backs may be implemented using class member functions. Member functions may access the object and the object's attributes for which they are invoked. Member functions look like closures. However, member functions are not closures because they are associated with classes. In other words, all the instances of a class share the implementation of all their member functions. That is, different call-back implementations require different class declarations. For instance, if one wants to implement a button with a call-back printing a plain message and another one emitting a sound, two classes have to be defined. These class declarations turn out to be an hindrance to simplicity and readability. In addition, if several events must be handled by one widget, this technique turns out to be impractical because it is not possible to define a new class for each kind of events the widget must react to (mouse-1, mouse-2, mouse-3, shift-mouse-1, ctrl-mouse-1, shift-ctrl-mouse-1, ...).To avoid these extra class definitions, Java has introduced inner classes. An inner class is a class defined inside another class; it may be anonymous. Because in Gui programming inner classes are used to implement call-backs and as they are numerous, Java proposes a new syntax that enables within a single expression, to declare and to instantiate an inner classe. For instance:
The expression new ActionListener... deserves some explanation: 1) it declares an anonymous inner class that 2) implements the
interface ActionListener and 3) it creates one unique instance
of that new class that is sent to the method addActionListener of
the Button instance. That is, anonymous inner classes are the exact
Java implementation of closures. It is worth pointing out that
Scheme syntax for closures is far more compact than the Java
one.3.6.3 ClosuresClosures are central to Gui programming because they are one of the most natural way to implement call-backs. As we have seen, all call-back based toolkits offer a mechanism similar to closures. It can be member functions or anonymous Java classes or extra parameter passed to C functions. However, we have found that solutions of non-functional programming languages are not as convenient as Scheme'slambda
expression because either the user is responsible of the construction
of the object representing the closure or extra syntax is introduced.
Not only call-backs are easily implemented by the means of closures but we have also found that closures are handy to implement pre-existing data structure visualization. Consider the screen shot, Figure 7. It is made of two Biglook trees. A Biglook tree is a visualization of an existing data structure. That is, a Biglook tree does not contain data by itself. It only visualizes an existing structure. A Biglook tree is defined by three slots: i) the root of the tree ( root ), ii) a function
that extracts a string which stands for the label of a node
(node-label ), iii) a function that computes the children list
of a node (node-children ). The declaration of the trees of
Figure 7 are given Figure 6,
lines 2 and 8. The first tree
(tree1 ) is a class tree, its root is the Scheme object class
denoting the root of the inheritance tree (line 4). The
second tree (tree2 ) is a file tree. Its root is "/" , the root of
the file system (line 10). To compute the name of a node
representing a class it is only required to extract the name of that
class (line 5). The name of a node representing a
directory is the node itself since the node is a string
(line 11). The children list of a class is computed
using the Bigloo library function class-subclasses
(line 6). The children list of a directory is computed
by the library function directory->list (line 12). As one
may notice ``hooking'' a tree to a data structure is a simple task.
The resulting program is compact. We think that this compactness is
another strength of Scheme closures.In general, the Biglook Api enables small source codes for Guis. On a typical graphical interface we have found that the Biglook source is about twice smaller than the same interface implemented in Tcl/Tk and about four times smaller than the interface implemented in C with GTK+. 4 Implementing BiglookIn this section, we present the overall Biglook architecture and implementation. Then, we detail the role of virtual slots.4.1 Library ArchitectureThe Biglook library is implemented on top of a native back-end toolkit (currently a GTK+ back-end and a Swing back-end are available). It takes advantage of the efficiency of the back-end low level operations and it imposes a low memory overhead (about 15% of additional allocations). The difference in execution time is marginal. As a consequence, Biglook can be used to implement applications using complex graphical interfaces such as the one presented Figure 1. Interested readers can find precise performance evaluation of these implementations in a previous technical report [12].Because Biglook makes few assumptions about the underlying toolkit, re-targeting its implementation to another back-end is generally possible. To be a potential Biglook back-end, a toolkit must provide: i) a way to identify a particular component of the interface. Of course all the toolkits provide this, even if the representation used can differ, such as an integer, a string, a function, and so on. ii) an event manager that does not hide any events. All the toolkits we have tried satisfy this criteria (Tk, GTK+ and Swing). In addition, if a back-end lacks some widgets, Biglook implements them using primitive widgets. Figure 8 shows the underlying architecture of the Biglook toolkit. Actual graphical toolkits generally support these prerequisites and, as such, can be used as potential Biglook back-end. We will see in Section 4.2 that using virtual slots allows the building of the rest of the library on this minimal basis. 4.2 Virtual Slots and BiglookA simple widget is a widget that is directly mapped into a builtin widget. All simple widgets are implemented according to the same framework: they inherit from thewidget class and they define user
customization options. These options are implemented using
virtual slots. We present here a possible implementation of the
label class. For the sake of simplicity, we assume that this class
extends the widget class with only one additional slot: the text
slot that specifies the label text.
Implementing the text slot requires virtual slots. Its
getter and setter functions directly interact with the back-end
toolkit. Virtual slots are used to establish the connection between
Biglook user point of view of widgets and their native implementation.
Virtual slots are used to provide an object oriented class-based
Api to Biglook, independent of the back-end.Now that the slots of a label widget are defined, we must define how such a widget has to be initialized. The class initialization specific code is given to the system via the generic function realize-widget . For each class of the library a method overrides this
generic function and must call the back-end to create the graphical
object associated with the class. For a label, the method we need to
write is:
This method creates a builtin label via the low level <builtin-make-label> function and stores the result in the builtin slot. This slot creates the link between the
Biglook toolkit and the back-end.5 Related workMany functional languages are connected to widget libraries especially to the Tk toolkit. Few of them use object-oriented programming except in the Scheme world, we can cite mainly STk, SWL and MrEd.5.1 Scheme widget librariesSTk [11] is a Scheme interpreter extended with the Tk graphical toolkit. To some extent, STk is the ancestor of Biglook, since it was developed by one of the authors of this paper. However, STk is tightly coupled to the Tk toolkit and even if this toolkit is presented to the user through an object oriented Api as in Biglook, no provision was made to be independent from this back-end.SWL is a contribution to the Petite Chez Scheme system [6]. It relies on an interpreter and uses Tk as back-end. In SWL, native Tk widgets are mapped to Chez Scheme classes and in this respect this toolkit is similar to the Biglook or STk libraries. However, SWL implementation is very different from the one used by those libraries since SWL widgets explicitly talk with a regular Tcl/Tk evaluator. MrEd [9], a part of the DrScheme project [7], is a programming environment that contains an interpreter, a compiler and other various programming tools such as browser, debugger, etc. The back-end toolkit used by MrEd is wxWindow [27], a toolkit that is available under various platforms (Unix, Windows, etc.). As STk, this toolkit is completely dependent of its back-end. 5.2 Other functional languages widget librariesOther functional languages provide graphical primitives using regular functions. The main contribution for strict functional programming languages has been developed for the Caml programming language [30].The first attempt, CamlTk, is quickly surveyed in [23]. The design of CamlTk is different from the one of Biglook. CamlTk binds Tk functions in Caml while Biglook provides an original Api made of classes. Recently a new widget library based on GTK+ has been proposed for Caml. No article describes that connection. However, some work has been described to add keyword parameters to Caml in order to help the connection with widget libraries [10]. The philosophy of that work differs from ours because that new library makes the GTK+ Api available from Caml. No attempts are made to present a neutral Api as we did for Biglook. Programming graphical user interfaces with lazy languages is far more challenging than with strict functional languages. The problem is to tame the imperative aspects of graphical I/O in such languages [18, 28]. Several solutions have been proposed: Fudget by Carlsson and Hallgren [2, 3], Haggis by Finne and Peyton Jones [8], TkGofer by Vullings and Claessen [4] and, more recently the extension of the former TclHaskell library: FranTk by Sage [24]. ConclusionIn this paper we have presented Biglook, a widget library for the Bigloo system. The architecture of Biglook enables different ports. Currently two ports are available: a GTK+ port and a Swing port. Biglook source code can be indifferently linked against the two libraries. Biglook Api uses an object oriented programming style to handle graphical objects and a functional oriented programming style to implement the interface reactivity. We have found that combining the two programming styles enables more compact implementations for Guis than most of the other graphical toolkits.AcknowledgmentsMany thanks to Keith Packard, Jacques Garrigue, Didier Remy, Peter Sander, Matthias Felleisen, Simon Peyton Jones and to Céline for their helpful feedbacks on this work.References
This document was translated from LATEX by HEVEA. |
This paper was originally published in the Proceedings of the FREENIX Track: 2002 USENIX Annual Technical Conference, June 10-15, 2002, Monterey Conference Center, Monterey, California, USA.
Last changed: 16 May 2002 ml |
|