Academia.eduAcademia.edu

Declarative event-oriented programming

2000, Proceedings of the 2nd ACM SIGPLAN international …

Events play an important role in the construction of most software that involves interaction or simulation. Typically, programmers make use of a fixed set of low level events supplied by a window system, possibly augmented with timers and UI components. Event handling generally involves some interpretation of these event occurrences, followed by external actions or modifications to program state.

Declarative Event-Oriented Programming Conal Elliott October 19, 1998 Technical Report MSR-TR-98-24 Microsoft Research Microsoft Corporation One Microsoft Way Redmond, WA 98052 Declarative Event-Oriented Programming Conal Elliott http://research.microsoft.com/~conal Microsoft Research Abstract Events play an important role in the construction of most software that involves interaction or simulation. Typically, programmers make use of a fixed set of low level events supplied by a window system, possibly augmented with timers and UI components. Event handling generally involves some interpretation of these event occurrences, followed by external actions or modifications to program state. It is possible to extend the event paradigm by using event interpretation to synthesize new kinds of events tailored specifically for a domain or application. In turn, these new events may be used to synthesize yet others, and so on, to an arbitrarily sophisticated degree. This programming paradigm, which we call event-oriented programming, aids in the factoring of programs into understandable and reusable pieces. We propose a declarative approach to event-oriented programming, based on a powerfully expressive event language with a lightweight notation. We illustrate this new approach through the design of an interactive curve editor. Keywords: events, behavior, interaction, declarative programming, animation. 1 Introduction The notion of event is central in the construction of most software that involves interaction or simulation. Such software is typically organized around a centralized event queue and the event loop that removes and acts on events. For example, under Microsoft Windows®, the operating system posts messages describing occurrences of user interaction events, such as mouse clicks, key presses, and window resizing. A Windows program repeatedly removes a message from the queue, and passes it to the window procedure, which examines its type and invokes appropriate application code. It is also often useful to add higherlevel event types. Sometimes, as in the case of menus, buttons and dialog boxes, these new types are packaged up into reusable widget libraries. In other cases, as in collisions during a game, the higher level events are about an application's content rather than its user interface. While the notion of an event is natural in many applications, the support provided by modern window systems has some serious weaknesses that interfere with program construction and maintenance. Some of these weaknesses stem from the fact that events are often just symbols with no intrinsic meaning. Consider the event of the user pressing the left mouse button. In Windows, this event is called WM_LBUTTONDOWN, and each occurrence contains a snapshot of the mouse position and the state of the control and shift keys. If an application should respond to all left button presses that its window receives, the programmer will write some window procedure code that looks like the following: switch (msg) { ... case WM_LBUTTONDOWN: keyState = wParam; xPos = LOWORD(lParam); yPos = HIWORD(lParam); ... // response goes here break; ... } Suppose that an event of interest is one of the following: Conal Elliott Declarative Event-Oriented Programming • the user pressing an arrow key; • the left button being pressed while the mouse is over an object of interest; • the same example, but with the event data being an index of the selected object; • the right and left buttons being pressed within 50 milliseconds of each other; or • the collision of two objects, with the event data being the instantaneous relative velocity. The nature of events as intrinsically meaningless identifiers prevents any of these examples from being encapsulated as an event. Of course, an application designer can still conceive of these events and implement detection and response to them. In the implementation, however, the represented events remain trivially simple, while the response code and supporting data structures and state variables become increasingly complex. GUI frameworks like Microsoft's MFC® and Visual Basic® help somewhat by breaking up event handling into separate methods, each with its own, tailored interface. However, the set of possible of event response methods is still limited to the same generic set. Moreover, all consequences of any given event are handled in a single handler method. An object-oriented representation of events, as in the Java 1.1 AWT event model, addresses some of these problems. Event response programming is even less monolithic than in MFC or Visual Basic, since a program may conveniently separate different responses to an event into different event “listeners” that may be registered and unregistered with an event, dynamically [11]. When an event occurs, a corresponding method is automatically invoked on each registered listener. The use of inner classes, a feature added in Java 1.1, is a considerable notational help. It is also possible to define arbitrary new kinds of events. However, doing so is tedious, because the framework's AWTEventMulticaster class (which manages listener lists) can only supply a fixed set of overloadings for the listener add and remove methods. New kinds of events may easily have signatures that do not match any of the given overloadings. In such a case, the programmer of the new kind of event must also implement all of the list management needed to support multiple listeners. This paper introduces an alternative paradigm we call declarative event-oriented programming that addresses the shortcomings mentioned above. 2 Declarative event-oriented programming The essential idea in this paper is to enrich the popular notion of events into a powerfully expressive language that includes not only primitive events, but also operators for building up more complex events from simpler ones. Benefits of this approach include the following. • Modularity/reuse. Programmers can factor tasks involving interactivity and reactivity into easily understood pieces, and develop libraries of reusable interaction components. Event handling is factored into a set of independent, incremental enhancements, encapsulated within the events that make up a program. • Lightweight notation. Programs describe high level events as what they are, not as sequences of steps to decode event data, test conditions, maintain global data structures, post events, etc. The notational style is algebraic, composing operators in succinct nested expressions, as in commonplace calculations on numbers. • Flexible naming. Events are completely independent from their naming, which fully exploits the naming mechanisms supported by a programming language, including lexical scoping, inclusion in objects and data structures, selective exportation from modules, and linking. Accidental name collisions are caught by the compiler or linker. As importantly, an event may be anonymous, being described merely in the construction of a more complex event. (The same property for numerical computation is a key advantage of high-level programming languages over assembly language.) • Safety. Definition and use of events are type-checked at compile time. In contrast, posting and decoding a Windows event requires unsafe type casting to and from the generic lParam and wParam. In contrast with the conventional approach, the events described in this paper have intrinsic meaning, based on a simple but quite general model. The meaning of an event is a sequence of occurrences, each of which is a time/value pair. Another notion, useful in conjunction with events, is the behavior, whose meaning is simply a function of continuous time. The event-forming operators discussed in this paper fall into the following categories: • transforming an event's data; Conal Elliott • forming the union of two events; and • filtering out some event occurrences. Declarative Event-Oriented Programming There are other operators as well, not illustrated in this paper: • monitoring time-varying conditions; and • sequential chains of events. These ideas have been implemented in Fran (“Functional reactive animation”). Previous papers [10][7][8] have presented Fran’s basic building blocks for reactive animation or have emphasized behaviors rather than events. In this paper, we focus on events, and by means of a running example, attempt to convey a new style for programming interaction applications, based on event-oriented design. Fran is a library for use with the functional programming language Haskell [15] [16]. We will explain specific features of Haskell as they arise, and refer the reader to [3] for an introduction to functional programming and [17] for motivation. See also [15] for an introduction to Haskell and [16] for reference. In brief, Haskell has the following properties. In brief, Haskell is purely functional, statically typed, polymorphic, higher-order, lazy, and syntactically flexible. • Purely functional. Programs are made up of expressions and functions that produce values, rather than statements and procedures that alter state. • Statically typed. All type errors are caught at compile-time. Moreover, types are inferred automatically when not stated explicitly. • Polymorphic. A single function can work on an infinite family of related types. • Higher-order. Functions are first-class values, and may be passed into or returned from functions. • Lazy. Computations are delayed until their results are needed. • Syntactically flexible. Operators, functions, and even constants may be overloaded in a systematic way. As explained in [7], these features make Haskell a near-ideal host language for “domain-specific embedded languages” like Fran. 3 The example In the next several sections, we will build up an interactive “poly-Bezier” curve editor. Bezier curves are popular in computer graphics and manufacturing because they are well behaved and visually pleasant. Each curve is defined by four control points. The first and fourth lie at the curve's endpoints, while the middle two typically lie off of the curve. These inner control points allow the user to tug at the curvature. A polyBezier curve is the union of a sequence of simple Bezier pieces, in which the first control point of each piece after the first one coincides with fourth control point of the previous piece, as in Figure 1. Figure 1. Poly-Bezier curve 4 Rendering control points The first step in our development is an editor for a single control point. We will represent a control point as a pair containing a trajectory and a status flag indicating whether it may be grabbed. type CPoint = (Point2B, BoolB) As a convention, the Fran type names “Point2B”, “BoolB”, etc are synonyms for “Behavior Point2”, “Behavior Bool”, etc, meaning time-varying 2D points, booleans, etc. We will want a control point's appearance to indicate whether it can be grabbed. Because this is the first example code, however, we will first consider how to render just a point, without this indication. We “render” a control point, i.e., turn it Conal Elliott Declarative Event-Oriented Programming into an image animation (ImageB), simply as a small shape, moved according to the given trajectory. The shape is a star when “excited” (grabbable) and a circle otherwise. 1 renderCPoint :: CPoint -> ImageB renderCPoint (pos, excited) = moveTo pos ( stretch pointSize ( ifB excited (star 3 5) circle)) pointSize :: RealB pointSize = 0.07 Functions like moveTo, stretch, and renderCPoint operate on time-varying values (behaviors). One may think of these operations as building up a “temporal display list” to be traversed iteratively by Fran during presentation. Lesson Motion and animation are not the byproducts of program execution, as in traditional imperative programming, but first-class values, expressed directly. 5 A simple control point editor Next comes the control point editor, which takes a static (not time-varying) start point and user input stream (represented in Fran by a value of type User), and synthesizes an interactive control point. The user can grab the control point with the left mouse button when the mouse cursor is close enough to it (within two radii).2 editCPoint :: User -> S.Point2 -> CPoint editCPoint u p0 = (pos, closeEnough) where pos, lastRelease :: Point2B pos = ifB grabbing (mouse u) lastRelease lastRelease = stepper p0 (release ‘snapshot_‘ pos) closeEnough, grabbing :: BoolB closeEnough = distance2 pos (mouse u) <* grabDistance grabbing = stepper False (grab -=> True .|. release -=> False) grab, release :: Event () grab = lbp u ‘whenE‘ closeEnough release = lbr u ‘whenE‘ grabbing grabDistance :: RealB grabDistance = 2 * pointSize Here are brief, informal descriptions of each definition within the “where” clause, from top to bottom. Technical details follow. 1 Haskellisms: The first line of this example is a type declaration for renderPoint, saying that it is a function from trajectories to animations. Although in almost all cases, the Haskell compiler or interpreter can infer types automatically, we give types explicitly to clarify the examples. In place of formal parameters, one may give patterns to be matched. In the second line, pattern matching pulls apart the single, pair-valued argument to renderCPoint, extracting the components pos and grabbable. Application of a function f to arguments x , y, ... is written simply "f x y ...". For example, the body of the definition of renderPoint is the application of the Fran moveTo function to two arguments, with the first being pos and the second being the application of the function stretch to pointSize and circle. 2 Haskellisms: The first line in this example says that editPoint is a function that takes a user value and a static point, and yields a control point. (The "->" type operator is right-associative, so literally, editPoint takes one argument and produces a function that takes another argument.) Function names made up of alphanumeric characters may be used as infix operators by surrounding them with backquotes, as in ‘whenE‘ below. Names made up of symbol characters, such as ".|." below, are treated as infix by default. The trivial type "()" is similar to C’s void type. Its one value contains no information. Conal Elliott Declarative Event-Oriented Programming • The position is defined as a behavior-level (time-varying) conditional: the mouse position when the user is grabbing, and the position of the last release otherwise. • The last release position is the start point at first, and then becomes a snapshot of the control point position whenever released. • The control point position is considered “close enough” to grab when it is within grabDistance (two radii) of the mouse's position. (The “<*” operator is the behavior-level version of the familiar less-than operator. For most functions an operators on numbers, the usual name is overloaded to mean the behavior-level version as well. In other cases, an asterisk or “B” is added to the end of the usual name.) • The boolean grabbing behavior is piecewise constant (a “step function”), initially false. It becomes true whenever the user grabs the control point, and false when the user releases it. • The grab event is considered to occur when the user presses the left button when “close enough”. • Similarly, the release event is considered to occur when the user releases the left button while grabbing. We can try out our simple control point editor by writing a tester function. This function combines editCPoint and renderCPoint, uses the static 2D origin as the starting position, and places the result over a white background. Note that an “interactive animation” is a function that maps user-input streams to animations. editCPointTest :: User -> ImageB editCPointTest u = renderCPoint (editCPoint u S.origin2) ‘over‘ whiteIm whiteIm :: ImageB whiteIm = withColor white solidImage Figure 2 shows the result, with user input echoed at the bottom of the window.3 The definition of editCPoint above illustrates declarative eventoriented programming. Some of the events are named (grab and release), while others are expressed anonymously in the construction of the behaviors grabbing and lastRelease. Although one may read these definitions idiomatically as in the informal explanation above, their meanings come precisely from the composition of independently meaningful operations. Consider first the definition of grab. The function lbp maps a user value into an event each of whose occurrences indicates that the given user's left Figure 2. editCPoint mouse button is being pressed. (Recall that an event is a sequence of 4 occurrences, each of which is a time/value pair. ) There is no useful explicit data, so it has the following type declaration. lbp :: User -> Event () The function “whenE” (here used in infix) acts to filter out some of the occurrences of an event. It allows through only those occurring when a boolean behavior is true. It has the following type declaration.5 whenE :: Event a -> BoolB -> Event a The definition of grabbing uses two more event-building operators: “-=>” and “.|.”. The “-=>” operator has higher syntactic precedence (binding more tightly), and has type 3 Figures like this one show snapshots of an animation. Read the top row from left to right, then the second row, and so on. To save space, many intermediate frames have been removed. See ftp://ftp.research.microsoft.com/pub/tr/tr-98-24/animations.htm for animated versions of all of the figures in this paper. 4 5 Typically, early occurrences need to be accessible before later ones can be known, so the sequences must be represented lazily. Haskellism: In type declarations, non-capitalized names, like a here, are type variables, indicating polymorphism, i.e., the ability to work with all types. For instance, whenE applies to any kind of event and yields an event of the same type. Conal Elliott Declarative Event-Oriented Programming (-=>) :: Event a -> b -> Event b It replaces the value in every occurrence of an event with a given value. Here, the value from grab, which is always the trivial value (of type “()”), is replaced by True, and the value from release, also trivial, is replaced by False, producing two new boolean events. The merge operator “.|.” has type (.|.) :: Event a -> Event a -> Event a Given events e1 and e2, the new event e1.|.e2 has as its occurrences the occurrences of e1 and e2 combined. In our example, the merged event occurs with value True whenever the grab event occurs and with value False whenever the release event occurs. The grabbing boolean behavior is then defined by applying the stepper function, which has the following type. stepper :: a -> Event a -> Behavior a The behavior it creates starts out with the value given by the first argument and changes with each occurrence of the event argument. In this case, the grabbing behavior starts False and switches to True when the control point is grabbed and False when the control point is released. Finally, the definition of lastRelease illustrates the use of events to make behavior snapshots. The event function used is snapshot_ :: Event a -> Behavior b -> Event b An event “e ‘snapshot_‘ b” occurs whenever e occurs, but its values are the values of b at the occurrence times. In our example above, the control point's position is snapshotted on each release, yielding a point-valued event, which is used to make the piecewise-constant lastRelease behavior. Lessons This first example of declarative event-oriented programming illustrates a few lessons. • Separate the model from the presentation (here a Point2B and an ImageB, respectively). • Remember with snapshot_ and stepper. • Enrich and merge events with “-=>” and “.|.”. • Specialize with whenE. 6 A first curve editor It is now a simple matter to put together a curve editor, representing curves as lists of control points. First define the appearance of a curve: rendered control points over a blue poly-Bezier curve. We use the Fran function overs to overlay the list of rendered control points, and its binary version over, to combine with the Bezier image.6 renderCurve :: [CPoint] -> ImageB renderCurve cpoints = overs (map renderCPoint cpoints) ‘over‘ withColor blue (polyBezier (map fst cpoints)) Next, define a curve editor. (The editCPoint function is being partially applied, yielding a function that is mapped over the initial points.) editCurve :: [S.Point2] -> User -> [CPoint] editCurve initPoints u = map (editCPoint u) initPoints 6 Haskellisms: For any type a, the type [a] contains all lists whose members are all of type a. Thus renderCurve is a function from control point lists to image animations. The map function takes a function and a list of values and yields a new list made up of the given function applied to each member of the given list. The fst function extracts the first member of a pair, in this case each control point’s position. Conal Elliott Declarative Event-Oriented Programming Finally, put the pieces together: given an initial list of static points, make an interactive curve, render the result, and place it over a graph paper background. editor :: [S.Point2] -> User -> ImageB editor initPoints u = renderCurve (editCurve initPoints u) ‘over‘ graphPaper Figure 3 shows a sample use of editor. Lesson Look for smaller constituent problems (e.g., point editing), especially those repeated in the larger problem. Solve the subproblems, test your solutions, and then compose them. A comparison Consider how one might implement the curve editor in the event loop style, say under Windows. Preserving most of the conceptual structure of the version above, it might work as follows. • Declare a global array of control point state data structures, initialized when a curve file is loaded. Minimally, each state could consist of a point and a boolean, corresponding to lastRelease and grabbing respectively. • On WM_LBUTTONDOWN: For each control point, if its position is close enough to the mouse position, set the grabbing flag to true. • On WM_LBUTTONUP: For each control point, if its grabbing flag is true, set its position to the current mouse position and set its grabbing flag to false. • On WM_TIMER: For each control point, if its grabbing flag is set, set its position. to the mouse’s. • On WM_MOUSEMOVE: If any control point has its grabbing flag set, mark the window as needing to be repainted, which will generate a WM_PAINT event. (A simpler, but less efficient, alternative is to to generate WM_PAINT events in response to a timer or in "idle processing".) • On WM_PAINT: First draw the graph paper background and the poly-Bezier curve. Then iterate through the control points. For each one, draw it at a position that is equal to the mouse’s position if control point’s grabbing flag is set, and equal to the lastRelease point otherwise. Figure 3. editor Notice that this event loop version would be much less modular and concise than the version given in Sections 5 and 6. The complexities of the point editor are exposed to the curve editor rather than neatly encapsulated. Consider what would happen if more features were added, such as sensitivity of the curve segments. The reactivity to user input of all kinds of elements would be mixed together in the single monolithic event loop. We can also compare to an object-oriented design. In Java, such a design could represent the control point states as objects in a new CPoint class derived from the appropriate event listener classes, and adding a display method. After these control point objects are created, they would be inserted into the appropriate listener lists. The reactions sketched just above to WM_LBUTTONDOWN, etc would be moved into the CPoint event handler methods. Like declarative programming, object-oriented design encourages explicit modeling of the conceptual entities in a task, moving complexity out monolithic bodies of code into well-insulated pieces that may then be composed. However, objectoriented programs typically embrace the imperative programming style within their methods, and in doing so compromise the principle of modeling. Moreover, object-oriented languages impose much more notational overhead. In this light, the style proposed in this paper may be seen as a stateless and extremely fine-grained form of object-orientation, with an unusually lightweight notation. (See [24] for a description of a fine-grained object-oriented implementation of a predecessor of Fran.) Conal Elliott 7 Declarative Event-Oriented Programming Control point editing with undo We now add the ability to undo an unlimited number of editing operations. Undo can be implemented by maintaining a stack of information with which to reset the state to what it was just before the change. Edits generate pushes and undos generate pops. Instead of building stack maintenance into our editor, we will implement a polymorphic, unbounded stack manager. A client feeds pushes and pop attempts in, and gets successful pops out. The stack itself is hidden in the stack manager’s implementation.7 stacker :: Event a -> Event () -> Event a stacker push tryPop = legitPop ‘snapshot_‘ headB stack where legitPop :: Event () legitPop = tryPop ‘whenE‘ notB (nullB stack) -- changeStack :: Event ([a] -> [a]) changeStack = legitPop -=> tail .|. push ==> (:) -- stack :: Behavior [a] stack = stepAccum [] changeStack The definition works by maintaining a list-valued behavior called stack, which is used and defined as follows. • The stacker function returns an event whose occurrences contain a snapshot of the top of the stack at each legitimate pop. • A legitimate pop is an attempt when the stack is not empty. • The changeStack event’s values are functions from stacks to stacks. A legitimate pop leads to popping (via the tail function), and a push with value x leads to pushing x (by partially applying the Haskell cons function “:” to x). • The stack starts out empty, and changes whenever changeStack occurs. The function associated with an occurrence of changeStack is applied to the previous value of the stack, giving a cumulative effect. The “==>” operator is similar to “-=>”, but applies a given function to each of its event argument's occurrence values. It has the following type. (==>) :: Event a -> (a -> b) -> Event b The function stepAccum builds a piecewise-constant behaviors by cumulatively applying occurrences of a function-valued event, and has the following (polymorphic) type: stepAccum :: a -> Event (a -> a) -> Behavior a Given the stacker function, it is now almost trivial to add undo to the control point editor. The only changes (shown in bold) are to consider undo events to be releases, to push on every grab event, and try to undo when the user presses control-Z (as detected by the Fran function charPress). editCPointUndo :: User -> S.Point2 -> CPoint editCPointUndo u p0 = (pos, closeEnough) where pos, lastRelease :: Point2B pos = ifB grabbing (mouse u) lastRelease lastRelease = stepper p0 (release ‘snapshot_‘ pos .|. undo) closeEnough, grabbing :: BoolB closeEnough = distance2 pos (mouse u) <* grabDistance grabbing = stepper False (grab -=> True .|. release -=> False) 7 Haskellism: Lines beginning with “--“ are comments. Due to a soon-to-be-remedied restriction in Haskell's type system, the polymorphic types of changeStack and stack cannot be given explicitly, so we insert them as comments. Conal Elliott Declarative Event-Oriented Programming grab, release :: Event () grab = lbp u ‘whenE‘ closeEnough release = lbr u ‘whenE‘ grabbing grabPos, undo :: Event S.Point2 grabPos = grab ‘snapshot_‘ pos undo = stacker grabPos (charPress ’\^Z’ u) Undoing works correctly in this new version, as shown in Figure 4. Lessons • The interface to stacker is that of a service, rather than a data structure. To form request channels, pass one or more event argument in to the service function. To get results out, return one or more events. Encapsulate internal state by means of locally defined behaviors and events. • By generalizing a specific requirement (undo) to a more general service (a polymorphic stacker), we avoid complicating the point editor, and we made a very reusable tool. The curve editor in next section benefits from this decision. • For incremental changes, use function-valued events and stepAccum, and leave the accumulation to Fran. 8 Curve editing with undo Unfortunately, the control point editor in the previous section is Figure 4. editCPointUndo not appropriate for making a curve editor with undo. The problem is that each control point has its own undo stack. When the user presses control-Z, all moved control points will back up. We can fix this problem by moving the undo stacking out of the point editor, where it is replicated, into the curve editor. The point editors can no longer determine the undo event by themselves, so they will instead be told as an argument. In addition, since the curve editor will be doing the stacking, the point editor must return the formerly private grab event. No other changes are required. editCPointUndo :: User -> Event S.Point2 -> S.Point2 -> (CPoint, Event S.Point2) editCPointUndo u undo p0 = ((pos, closeEnough), grabPos) where pos = ifB grabbing (mouse u) lastRelease lastRelease = stepper p0 (release ‘snapshot_‘ pos .|. undo) closeEnough = distance2 pos (mouse u) <* grabDistance grabbing = stepper False (grab -=> True .|. release -=> False) grab = lbp u ‘whenE‘ closeEnough release = lbr u ‘whenE‘ grabbing grabPos = grab ‘snapshot_‘ pos The new curve editor below stacks the position of a point being moved, together with which point. The individual control point grab events are tagged each with its index and then combined with anyE, which is the “.|. ” operator applied to lists of events. The resulting curveGrab event is used for stacking, and the resulting undo event is then filtered for each Conal Elliott Declarative Event-Oriented Programming control point, using the suchThat event operator. Only undos with the appropriate index are passed through, and the indices are dropped.8 type UndoRecord = (Int, S.Point2) editCurveUndo :: [S.Point2] -> User -> [CPoint] editCurveUndo initPoints u = cpoints where -- Tag and merge the CPoint grab events curveGrab :: Event UndoRecord curveGrab = anyE (zipWith tag indices pointGrabs) where tag i e = e ==> (i ‘pair‘) -- pair i with e’s occurrence data indices = [1 .. length initPoints] -- The undo event: stack curve grabs and try to restore on control-Z’s undo :: Event UndoRecord undo = stacker curveGrab (charPress ’\^Z’ u) -- Edit an indexed CPoint. editCP :: Int -> S.Point2 -> (CPoint, Event S.Point2) editCP i p0 = editCPointUndo u undoThis p0 where -- Undo if a point tagged i comes off -- the undo stack. Drop tag. undoThis = undo ‘suchThat‘ ((== i) . fst) ==> snd -- Apply editCP to corresponding indices -- and initial points, and split (unzip) -- the resulting cpoints and grabs into two -- lists. (cpoints, pointGrabs) = unzip (zipWith editCP indices initPoints) This new curve editor correctly supports undoing. See Figure 5. Lessons • As with stacker, the use of events as arguments and return values in editCPoint sets up communication channels between concurrent activities, while local definitions serve to insulate each from irrelevant details in the other’s inner workings. In fact, exactly this mechanism is used to communicate user interaction to an animation. A User value is essentially an event over values of type UserAction. • Event tagging and event filtering are dual techniques, used to merge and separate sets of events. They allow one program component (editCurveUndo here) to act as a “post office”, collecting and routing messages. 9 Saving curves We next add the ability for the user to save an edited curve, by pressing the ‘s’ key, or by closing the editor window. The editor will assure the user that the curve is being saved, by displaying a spinning message, thanks to the following function, which takes a message and an event that indicates when to save, and yields an animated image. (Haskellism: An alternative notation for function application is the right-associative, infix operator “$”. Because it has very low syntactic precedence, it is sometimes used to reduce the need for parentheses.) 8 Haskellisms: The function zipWith is a variant of map for functions of two arguments. The operator "." means function composition. An infix operator, such as "==" and ‘pair‘ below, may be surrounded by parentheses and given an argument on the left or right, yielding a function that takes the missing argument and applies the operator. Conal Elliott Declarative Event-Oriented Programming Figure 5. editor with undo spinMessage :: String -> Event a -> ImageB spinMessage message saveE = stretch saveSize $ turn saveAngle $ withColor (colorHSL saveAngle 0.5 0.5) $ stringIm message where saveDur = 1.5 -- artificial duration sinceSave = switcher saveDur (timeSinceE saveE) -- Fraction remaining (one down to zero) saveLeft = 0 ‘max‘ (saveDur - sinceSave)/saveDur saveSize = 2.5 * saveLeft saveAngle = 2 * pi * saveLeft The amount of time since the last save event is defined to be saveDur initially, and changes each time a save occurs to the (time-varying) length of time since that occurrence. The timeSinceE function is not built into Fran, but may be defined easily, as follows. timeSinceE :: Event a -> Event TimeB timeSinceE e = e ‘snapshot_‘ time ==> since where since :: Time -> TimeB since t0 = time - constantB t0 At each occurrence of a given event e, the time is snapshotted to determine the occurrence time, which is passed to the since function, which converts it from a static number into a constant time-valued behavior (with constantB), and then subtracts the result from the running time. Conal Elliott Declarative Event-Oriented Programming By using “snapshot_”, “==>” and a few other basic event operators, we can create highly reusable building blocks like timeSinceE, or very task-specific events like sinceSave The switcher function is like stepper, but takes an initial behavior and a behavior-valued event yielding behaviors to switch to. In fact, stepper is defined simply in terms of switcher, using constantB to turn static values into constant behaviors, as follows. switcher :: Behavior a -> Event (Behavior a) -> Behavior a stepper x0 e = switcher (constantB x0) (e ==> constantB) To see spinMessage work, the following definition use the message “goodbye” and restart whenever a key is pressed, as shown in Figure 6. spinMessageTest u = spinMessage "goodbye" (keyPressAny u) Figure 6. spinMessageTest The curve editor becomes more complicated in order to accommodate saving curves. For one thing, we use a more general variant of displayU: displayUIO :: (User -> (ImageB, Event (IO ()))) -> IO () The argument to displayUIO constructs not only an image animation, but also an action-valued event. On each occurrence of the event, the corresponding action is executed. In this case, the action saves a curve snapshot in a file. Here is the new editor definition, followed by some brief explanation. Figure 7 shows the execution. editor2 :: String -> IO () editor2 fileName = do initPoints <- load fileName displayUIOMon (editRenderSave initPoints) where editRenderSave initPoints u = ( spinMessage "Saving ..." saveNow ‘over‘ renderCurve xPoints ‘over‘ graphPaper , doSave ) where xPoints = editCurve initPoints u ptsB = bListToListB (map fst xPoints) saveNow = charPress ’s’ u .|. quit u doSave = saveNow ‘snapshot_‘ ptsB ==> save fileName Conal Elliott Declarative Event-Oriented Programming Figure 7. Editor with curve saving Some explanation: • The animation part of editRenderSave, as used by displayUIO, is the spinning message animation overlaying the rendering of the curve being edited. The animated message “Saving ...” is shown when saveNow occurs, which is defined to be whenever the user presses the ‘s’ key or closes the window. • The action-valued event doSave also depends on saveNow. At each occurrence, the list of curve points is snapshotted, and passed as the second argument to the save function, which gets the original file name as its first argument. Snapshotting the control points is a bit tricky. Since renderPoints creates a list of pairs, we extract the first half of each pair (via “map fst”), and convert the resulting list of behaviors into a behavior over lists. 10 Relative Motion The previous versions had a bug: control points recenter on the mouse when grabbing. Consequently, two control points can easily become permanently stuck together: The problem is that the control point's position is defined to be equal to the mouse's while the control point is being grabbed: pos = ifB grabbing (mouse u) lastRelease lastRelease = stepper p0 (release ‘snapshot_‘ pos) Instead, we would like pos to be given the same movement as the mouse relative to the locations at the time of grabbing. The following revised definition achieves this relative motion. Conal Elliott Declarative Event-Oriented Programming editCPoint :: User -> S.Point2 -> CPoint editCPoint u p0 = (pos, closeEnough) where pos = switcher (constantB p0) ( grab ‘snapshot‘ mouse u ==> relMotion .| . release ==> constantB ) where relMotion (p, mp) = constantB p .+^ (mouse u.-. constantB mp) grab = lbp u ‘whenE‘ closeEnough ‘snapsho t_‘ pos release = lbr u ‘whenE‘ grabbing ‘snapsho t_‘ pos grabbing = stepper False (grab -=> True .|. release -=> False) closeEnough = distance2 pos (mouse u) <* grabDistance For convenience, we have changed the grab and release events to contain the snapshotted point’s position. The new definition of pos additionally snapshot the mouse’s position. It then feeds the resulting pair to relMotion, which adds the control point snapshot to the vector difference between the mouse position and the snapshot of the mouse position. Thus the control point gets the same relative motion as the mouse. The other position-changing event is the control point’s release, which gets converted from a static point to a constant behavior. Aside: by convention, names end in “_” indicate a function that forgets something, and have a nonforgetful version. For example, the nonforgetful snapshot function has the following declaration. snapshot :: Event a -> Behavior b -> Event (a, b) The forgetful version used in the previous editor versions is defined in terms of the nonforgetful version. Event operators like snapshot, whenE and “==>” are all left-associative and of equal precedence so that they may be cascaded without parentheses, as in this definition and the most recent definition of pos within editCPoint. 11 Related work Direct Animation is a library developed at Microsoft to support interactive animation [17]. It is designed for use from mainstream imperative languages such as Java, and mixes the functional and imperative approaches. Fran and Direct Animation both grew out of an earlier design called ActiveVRML [6]. Esterel [2][1] is a language for synchronous concurrent programming, especially useful in real-time programming. It adopts a synchronous model of concurrency, rather than the more popular asynchronous models, as in CSP [14]. Berry and Gonthier [2] point out that “deterministic concurrency is the key to the modular development of reactive programs and ... is supported by synchronous languages such as Esterel.” Communication is based on a instantaneous broadcast model. In contrast, asynchronous models lead to competition for communication, by removing queued events, which leads to nondeterminism. Fran shares these basic properties of instantaneous, synchronous, broadcast communication and the consequent determinism. Unlike Esterel, however, Fran has an additional continuous model of time (which is not the focus of this paper). Another difference is that Esterel has an extremely efficient implementation, based on compilation of concurrent programs into deterministic sequential automata. Perhaps the most obvious difference is that Esterel is an extension of imperative programming, while Fran adopts a functional style. Like Esterel, Lustre [13] and Signal [12] are based on discrete, synchronous, concurrent programming models, and have efficient implementations based on compilation into sequential automata. Unlike Esterel, they are declarative, and so Fran is more closely related to them. Lustre has counterparts to several of the primitive and derived constructs in Fran, including when, and variants of stepper, and self-snapshotting behaviors (Lustre's “pre” operator). The feel is different from Fran, due to the absence of continuous behaviors. CML (Concurrent ML) formalized synchronous operations as first-class, purely functional, values called “events” [23]. Fran's event operators “.|.” and “==>” correspond to CML's choose and wrap functions. Also like Fran, CML embeds its event vocabulary in an existing programming language. There are substantial differences, however, between the meaning given to “events” in these two approaches. In CML, events are ultimately used to perform an action, such as reading input from or writing output to a file or another process. In contrast, our events are used purely for the values they generate. These values often turn out to be behaviors, although they can also be new events, tuples, functions, etc. In addition, CML is built on a CSP-like nondeterministic rendezvous model for communication, rather than instantaneous broadcast. Similarly, Concurrent Conal Elliott Declarative Event-Oriented Programming Haskell [18] adds a small number of primitives to Haskell, and then uses them to build several higher-level concurrency abstractions. Unlike the Esterel family of languages (and Fran), CML and Concurrent Haskell add concurrency nondeterministically. Squeak [4] was a small language in the CSP tradition, designed to simplify development of user interfaces. Like the Esterel family of languages, Squeak compiles into an efficient sequential state machine. NewSqueak [21] extended the ideas in Squeak to a full programming language, having a C-like syntax, and anonymous functions. Pike then built a complete window system in fewer than 300 lines of NewSqueak [21]. Scholz described a “monadic calculus of synchronous imperative streams” that has a stream type, somewhat similar to Fran's behaviors, but based on a discrete model of time [25]. Its imperative nature allows one to perform side effects at any level of a behavior, while Fran imposes a discipline, allowing side effects only through action-valued events. 12 Future work The example presented in this paper is of modest complexity. Examples that are more ambitious will probably suggest improvements in the vocabulary of events, as well as shed more light on design methodology. Efficient implementation of Fran continues to be a challenging problem. The embedding of Fran in Haskell gives rise to considerable flexibility and expressiveness, as well as a reasonably simple implementation, but has been more difficult to optimize than we expected. In contrast, the efficiency of the Esterel languages is quite impressive. See [9] for a discussion of several approaches to implementing Fran. Debugging and performance analysis are also serious problems in any very high level paradigm, because of the large gap between the programmer's model and the program's execution. Although Fran is embedded in Haskell, which is a very expressive programming language, we do not propose Fran/Haskell as an application programming language. We believe that pragmatics require a more modest approach, namely using Fran to generate Haskell-based software components [20]. This language hybrid approach allows, for instance, the curve editor to rely on the facilities of a mainstream programming language to provide a modern graphical user interface, file I/O, etc. 13 Conclusions Declarative event-oriented programming makes it convenient to encapsulate significant portions of an interactive application into a set of high level, reusable building blocks. These events appear to capture important aspects of an application's design directly, and so may be useful in reducing the cost of creation and maintenance of modern interactive software. As software becomes increasingly powerful, its internal complexity tends to grow. As Edsger Dijkstra pointed out in his Turing Award lecture, the ideas we can express, and even think, are greatly influenced by the languages we use. In order to achieve our software-building ambitions, we therefore need languages that support abstraction and factoring of algorithms to form “intellectually managable programs”. One hopes that tomorrow's programming languages will differ greatly from what we are used to now: to a much greater extent than hitherto they should invite us to reflect in the structure of what we write down all abstractions needed to cope conceptually with the complexity of what we are designing. [5] References [1] Gérard Berry, The Esterel Primer, Book in preparation. Draft: ftp://ftp-sop.inria.fr/meije/esterel/papers/primer.ps.gz. [2] Gérard Berry and Georges Gonthier, The Synchronous Programming Language ESTEREL: Design, Semantics, Implementation, Science of Computer Programming, vol. 19, n.2 (1992) 83-152. ftp://ftp-sop.inria.fr/meije/esterel/examples/hdlc.ps.gz. [3] Richard Bird and Philip Wadler, Introduction to Functional Programming, Prentice-Hall, International Series in Computer Science, 1987. [4] Luca Cardelli and Rob Pike, Squeak: A Language for Communicating with Mice, Computer Graphics (SIGGRAPH ’85 Proceedings), Vol. 19, pp. 199-204, July 1985. [5] Edsger W. Dijkstra, The Humble Programmer (Turning Award lecture), Communications of the ACM, 15(10), October 1972. Conal Elliott Declarative Event-Oriented Programming [6] Conal Elliott, A Brief Introduction to ActiveVRML, Technical Report MSR-TR-96-05, Microsoft Research, http://research.microsoft.com/pubs. [7] Conal Elliott, Modeling Interactive 3D and Multimedia Animation with an Embedded Language. In the Proceedings of the first conference on Domain-Specific Languages, October 1997. www.research.microsoft.com/~conal/papers/dsl97/dsl97.html. [8] Conal Elliott, Composing Reactive Animations, Dr. Dobb’s Journal, July 1998. Expanded version with animated GIFs: http://www.research.microsoft.com/~conal/fran/tutorial.htm. [9] Conal Elliott, Functional Implementations of Continuous Modeled Animation, in the proceedings of PLILP/ALP ’98, Expanded version at http://research.microsoft.com/pubs. [10] Conal Elliott and Paul Hudak, Functional Reactive Animation, in Proceedings of the 1997 ACM SIGPLAN International Conference on Functional Programming, http://www.research.microsoft.com/~conal/papers/icfp97.ps [11] David Flanagan, Java Examples in a Nutshell: A Companion Volume to Java in a Nutshell. Published by O’Reilly & Associates [12] Thierry Gautier, Paul le Guernic and Loic Besnard, SIGNAL: A Declarative Language for Synchronous Programming of Real-Time Systems, Functional Programming Languages and Computer Architecture, pp. 257-277, Springer-Verlag, 1987. [13] N. Halbwachs and P. Caspi and P. Raymond and D. Pilaud, The Synchronous Data Flow Programming Language LUSTRE, Proceedings of the IEEE, 79(9), pp. 1305-1320, September 1991. [14] C. A. R. Hoare, Communicating Sequential Processes, Communications of the ACM, 21(8), pp. 666-677, August 1978. [15] Paul Hudak and Joseph H. Fasel, A Gentle Introduction to Haskell. SIGPLAN Notices, 27(5), May 1992. See http://haskell.org/tutorial/index.html for latest version. [16] Paul Hudak, Simon L. Peyton Jones, and Philip Wadler (editors), Report on the Programming Language Haskell, A Nonstrict Purely Functional Language (Version 1.2). SIGPLAN Notices, March, 1992. See http://haskell.org/report/index.html for the latest version. [17] John Hughes, Why Functional Programming Matters. The Computer Journal, 32(2), pp. 98-107, April 1989. http://www.cs.chalmers.se/~rjmh/Papers/whyfp.ps. [18] Microsoft, DirectAnimation, in the Microsoft DirectX web page, http://www.microsoft.com/directx. [19] Simon Peyton Jones, Andrew Gordon and Sigbjorn Finne, Concurrent Haskell, 23rd ACM Symposium on Principles of Programming Languages, pp. 295-308, 21-24 January 1996, http://www.dcs.gla.ac.uk/fp/authors/Simon_Peyton_Jones/concurrent-haskell.ps.gz. [20] Simon Peyton Jones, Erik Meijer, and Daan Leijen, Scripting COM Components in Haskell., Software Reuse 1998, http://www.dcs.gla.ac.uk/~simonpj/com.ps.gz. [21] Rob Pike, Newsqueak: A Language for Communicating with Mice, CSTR143, Bell Labs, March 1989, http://cm.bell-labs.com/cm/cs/cstr/143.ps.gz. [22] Rob Pike, A Concurrent Window System, Computing Systems, 2(2), pp. 133-153, spring 1989. [23] John H. Reppy, CML: A Higher-order Concurrent Language. Proceedings of the ACM SIGPLAN ’91 Conference on Programming Language Design and Implementation, pages 293-305, 1991. [24] Greg Schechter, Conal Elliott, Ricky Yeung and Salim Abi-Ezzi [1994], Functional 3D Graphics in C++ - with an Object-Oriented, Multiple Dispatching Implementation, in Proceedings of the 1994 Eurographics Object-Oriented Graphics Workshop. Springer Verlag, http://www.research.microsoft.com/~conal/papers/eoog94.ps [25] Enno Scholz, A Framework for Programming Interactive Graphics in a Functional Programming Language, PhD Dissertation, Freie Universität Berlin, 1998