Simplicity and Compositionality in Asynchronous Programming through First Class Events (Article Version)
Updates to this article from the original blog version based on reader comments are marked in purple!
As of version 1.1.10, F# now supports first-class, composable events. Here 'events' is used in the same sense as in the C# langauge, but you'll notice some important differences in how events appear in the language design and in practical F# programming. In this article I take a look at some samples using events in F# and explain how you can do compositional programming with events reminiscent of LINQ and functional programming. Much of what is presented here is inspired by James Margetson's personal use of a related event model called 'wires'.
For those unfamiliar with events, they are best understood as being the means by which the wiring of a GUI application is specified. For example, here is a simple GUI program in F#:
open
System
openSystem . Windows . Forms
let form = newForm ()
do form. Text <- "Hello World Form"
// Menu bar, menus
let mMain = form. Menu <- newMainMenu ()
let mFile = form. Menu . MenuItems . Add ( "&File" )
let miQuit = newMenuItem ( "&Quit" )
let _ = mFile. MenuItems . Add (miQuit)
// callbacks
do miQuit. Click . Add ( fun _ -> form. Close ()) The last line is the one we're interested in:
do miQuit. Click . Add ( fun _ -> form. Close ())
Informally the line just says "when the user clicks on the menu item, close the form". Here miQuit. Click is an event, and miQuit. Click.Add is a function that can be used to add a callback that listens to the event, i.e. is run when the event fires. The code fun _ -> form. Close () is an anonymous function value that implements the callback. Below are some other samples of event handlers, taken out of context from the samples available as part of the F# distribution. The last shows how events carry arguments - in this case an argument of type PaintEventArgs, as you would quickly discover if you hover the mouse
do
form. Resize . Add ( fun _ -> form. Invalidate ());
do form. Closing . Add ( fun _ ->Application . ExitThread ())
do form. Paint . Add ( fun ev -> guiRefresh ev. Graphics )
Aside: Users of earlier versions of F# will notice improvements here: prior to version 1.1.10 you had to write the following:
do form. add_Paint (new PaintEventHandler( fun _ ev -> guiRefresh ev. Graphics ))
Events as First Class Values
Now, the point of this article is not that syntax for 'add' on an event has changed - it is that events are now first-class values in the F# langauge. Indeed, events are not a separate notion at all in the language design, rather, events are just values of type Microsoft.FSharp.Idioms.IEvent<_>, and .NET events are effectively just properties of this type. This has very important ramifications: we've made the language both simpler (events only appear as a footnote in the language specification in relation to .NET interop), less restricted (fewer adhoc restrictions, as we will see) and more powerful (events can be composed and transformed, as we shall see). This, of course, harks back to an essential theme in the design of functional languages: where possible language design elements should be orthogonal, unified and first-class, and good things flow as a result.
Let's dig into this a bit with some more examples. Events are just values. For example, that means that form. Paint is a value, in particular of type IEvent < PaintEventArgs >. (Actually the type is a slight subtype refinement of this, called IDelegateEvent, which we'll come to later). Here the type parameter <PaintEventArgs> is the type of the argument passed when the event is triggered.
Events-as-values implies they can be passed around, for example:
open
Idioms
let addClose (e: # IEvent <_>) =
e. Add ( fun _ -> form. Close ())
do
addClose miQuit. Click
let buttonQuit = newButton ()
do addClose buttonQuit. Click
Here addClose is a function that accepts an event as an argument (the # means any subtype of IEvent is accepted, and the underscore means we don't care what type of argument is carried by an event). We use it to add a 'close-the-form' callback to both miQuit. Click and buttonQuit. Click.
New Events by Transformation and Filtering
Passign events as values is simple enough. The next thing to notice is that events can be transformed. For example, if we have a System.Diagnostics.Process handle called cmdProcess we can await asynchronous events that occur when data is emitted on the output channel - i.e. receiving output triggers the cmdProcess. OutputDataReceived event. The type of data carried is DataReceivedEventArgs. which is really just a string carried in the Data property. Using event transformate can extract the string as follows:
let outE =
cmdProcess. OutputDataReceived |> IEvent .map ( fun data -> data. Data )
The all-important |> operator was described in my previous post on LINQ. The result outE is a new event value, where a trigger of the original OutputDataReceived event will cascade to a trigger of outE. Here the type of IEvent.map is the very pleasing:
val
IEvent.map: ('a -> 'b) -> # IEvent <'a> ->IEvent <'b>
(Aside: IEvent.map comes from the module Microsoft.FSharp.MLLib.IEvent. Most common F# types such as String, List, IEnumerable and IEvent have a corresponding module under Microsoft.FSharp.MLLib that contains operations associated with the type. A key feature is that types such as interfaces can be given associated operations)
Not the similarity to map on IEnumerables, which manifests itself in LINQ as 'select'.
Events can also be filtered using IEvent.filter:
val
filter: ('a -> bool) -> # IEvent <'a> ->IEvent <'a> Here's a sample from the ConcurrentGameOfLife sample in the F# distribution:
let
mouseMove =
form. MouseMove
|> IEvent .filter ( fun e -> e. Button = MouseButtons . Left )
|> IEvent .filter ( fun _ -> inputMenuItem. Checked )
This filters triggers of form.MouseMove to generate a new event that only fires when the Left mouse button is down and a particular menu item is checked. As you can see, common patterns of event composition, filtering, combination and transformation can now begin to be abstracted.
After a while programming with F# events you begin to use filter and map and other event combinators without really thinking about it, just as you do for lists, arrays and other computational structures. For example, here's some code (using some identifiers not defined here) from the implementation of F# Interactive in Visual Studio - we won't explain it in full, but note the use of IEvent.map and other user-defined combinators such as bufferEvent and splitEvent. We'll show how to define your own event combinators later in this article.
let processOutput (outE : IEvent<_> ) =
let terminatedE,outE =
outE |> splitEvent ( fun s ->match s with
| null->None
| s ->Some (s)) inlet outE =
outE
|> IEvent .map fix
|> bufferEvent 50
|> IEvent .map ( String .concat "" ) in
terminatedE,outE
Digging Deeper: the IEvent type
Before we look at additional combinators and creating new events, it's time we looked at the IEvent type (we cover its refinement IDelegateEvent below). The definition of IEvent can be found in idioms.fsi in the F# distribution and IEvent is simply an interface type for objects with an 'Add' method:
type
IEvent <'a> =
interface/// Connect a listener function to the event. The listener will
/// be invoked when the event is fired.
abstractAdd : ('a -> unit) -> unit
end
That is, an event is just an object value that mediates access to an underlying set of event listeners through the Add method. You never actually need to if or how the event handlers are stored or triggered. IDelegateEvents (see below) also support AddHandler and RemoveHandler.
More on Events as Values: Creating Events
One of the restrictions of C# is that events can only exist as members within classes. With the F# model, new event values can be created just as values as part of any expression. We've already seen some examples of creating events using combinators like map and filter. I now want to show you the definitions of map and filter, so you'll be able to see how simple they are and how to create your own event combinators. But first we need to know how to create individual events. We could do this by explicitly implementing the IEvent interface, and managing our own backing store of event handlers. However, it's easier to just use the 'create' function in the IEvent module:
val
IEvent .create : unit -> ('a -> unit) * IEvent <'a>
This returns a trigger for the event and the event itself. For example, here we create an event:
let
(doDrawScene,drawScene) = IEvent .create()
And here is a longer sample where we create an event synthesized from the MouseUp, MouseDown and MouseMove events on a control to report both the current event arguments and the previous event arguments. The transformed event maintains private state in a reference cell.
let
mkMouseTracker (c : # Control ) =
let fire,event = IEvent .create() in let lastArgs = ref Nonein
c. MouseDown . Add ( fun args -> lastArgs := Some args);
c. MouseUp . Add ( fun args -> lastArgs := None );
c. MouseMove . Add ( fun args ->match !lastArgs with
| Some last -> fire(last,args); lastArgs := Some args
| None-> ());
event
This function is used in one of the F# DirectX samples to modify the DirectX view parameters based on the differential movement of the mouse:
let
mouseEvent = mkMouseTracker form
do mouseEvent. Add ( fun (args1,args2) -> move view args1 args2)
The Implementation of the IEvent Module
Now, we get to the definition of some of the IEvent module functions and combinators. (Note there's nothing to stop you defining your own such implementation - the IEvent type is not tied to any particular implementation of events.) The full explanation of these is left as an exercise for the reader, but they should be fairly easy to follow. Firstly, IEvent.create:
let create() =
let listeners = ref [] in
let trigger = ( fun x ->List .iter ( fun f -> f x) !listeners) in
let event = { newIEvent <'a>
withAdd (f) =
Idioms .lock listeners ( fun () ->
listeners := (!listeners)@[f]
)
} in
trigger,event
Secondly, the somewhat simpler IEvent.map:
let map f (w: # IEvent <'a>) =
let outw,oute = create() in
w. Add ( fun x -> outw(f x));
oute
and IEvent.filter:
let filter f (w: # IEvent <'a>) =
let outw,oute = create() in
w. Add ( fun x ->if f x then outw x);
oute
Note that the trigger function is almost invariable kept private in an event combinator. In a certain logical sense this corresponds to the privacy attained by the C# restriction that triggering can only happen from the text of the class where the event resides, though in F# we can use dead-simple procedural abstraction or the other hiding mechanisms available in the language (e.g. module signatures) to attain this privacy rather than adhoc language restrictions.
More Sample Combinators: Catching errors, Partitioning Events
Here is a combinator that catches any errors that occur on the event processing stream and throws up a message box instead of propagating the error back to the caller (this one is not in IEvent - we define it as part of the implementation of F# Interactive).
let
catchAll (ie: # IEvent <_>) : IEvent <_> =
let w,e = IEvent .create() in
ie. Add ( fun x ->
try w(x)
with err -> ignore( MessageBox . Show (err. ToString ())); ());
e
val
catchAll: # IEvent <'a> ->IEvent <'a> Here's it in action - this is the very line that protects VisualStudio from crashing when an error occurs while processing returned output from a slave F# Interactive process. (Using combinators is very effective here as it enables us to quickly visually check that asynchronous callbacks are protected in an appropriate way)
let outE = cmdProcess. OutputDataReceived |> catchAll |> IEvent .map ( fun data -> data. Data )
Another one I like is a function to partition an event into two events based on an predicate:
val partition: ('a -> bool) -> # IEvent <'a> -> ( IEvent <'a> * IEvent <'a>)
There are several other interesting combinators in the IEvent module, including ones for iteration and folding.
IDelgateEvent and the connection to .NET events
Finally, a word on IDelegateEvent and removing handlers. IDelegateEvent is a refinement of this primarily used by the F# compiler, though you will see it as part of intellisense or if you define your own event types. It correesponds to the .NET Framework event idiom as found in C# code. When F# sees a C# or Visual Basic class containing an event that conforms to this idiom (essentially all events do), it extracts the relevant delegate type and argument type associated with the event and makes the event available as a property of type IDelegateEvent:
type
IDelegateEvent <'del,'args> =
interface
inheritIEvent <'args>
/// Connect a handler delegate object to the event. A handler can
/// be later removed using RemoveHandler. The listener will
/// be invoked when the event is fired.
abstractAddHandler : 'del -> unit
/// Remove a listener delegate from an event listener store
abstractRemoveHandler : 'del -> unit
end
So, for example, form. Paint is actually a value of type IDelegateEvent < PaintEventHandler,PaintEventArgs >. You can use the AddHandler and RemoveHandler methods to explicitly add and remove delegates callbacks of the given type from the event. The model is that F# function values given to IEvent.Add can never be removed (partly because we try to avoid programming with function values as if they had object identity, since there is no explicit 'new' construct for function values), whereas delegate handlers can be removed from events. It is much rarer to need to remove event handlers from events, which is why this model makes sense.
IDelegateEvent also serves another role: if you want a event property of an F# class or augmentation to be published as a .NET event with associated .NET metadata then you have to ensure it has type Idioms.DelegateEvent. As a convenience the related type abbreviation Idioms.IHandlerEvent is also provided by F#.
Summary
In all, the F# approach to events bring major advantages to the complex portions of code that constitute the 'wiring' of .NET applications, particularly when programming involving asynchronous or reactive-feedback.
- The F# langauge specification is made simpler (events are just values and .NET events are just properties)
- Events-as-values enables common patterns of wiring to be abstracted and common methods of event synthesis and construction to be reused.
- The elements of the F# langauge design converge to a allow powerful combinatorial approach to event composition, transformation and selection.
We hope you enjoy programming with F# events!
The F# team, 24/03/2006
Comments
Anonymous
March 23, 2006
The text of this post is also available as an article, which I'll modify with latest material on this...Anonymous
November 10, 2006
Is this 'wire' model described somewhere in literature?Anonymous
May 17, 2007
This is reactive programming (dataflow programming). It would be more interesting to see the more established 'arrows' abstraction for such programming models introduced into the F# library (or even the language).Anonymous
May 17, 2007
Hi Yang, To some extent I agree. The mechanism described here is really designed to familiarize .NET programmers with the notion of compositionality in reactive programming. This is extremely useful (and hence interesting) to practicing .NET programmers. From an academic persepctive arrows are more interesting and, frankly, astoundingly beautiful. We've recently begun looking at technique to integrate arrow notation into F#. Thanks for your comment! donAnonymous
September 05, 2007
Is it possible to attach an event to, for example, the node of a treeview, before it is added to the controls of a form?Anonymous
January 22, 2009
Any further development on arrows in F#?Anonymous
January 11, 2010
The comment has been removedAnonymous
December 17, 2012
Hi Don and Tomas, and Yang. Re: "Arrows" From Evan Czaplicki's Elm -- Arrowized FRP : "... focus on three major semantic families of Functional Reactive Programming: Classical FRP; Real-time FRP and Event-Driven FRP; and Arrowized FRP. We examine them chronologically to see how the semantics of FRP have evolved. As we move through FRP's three semantic families, we will better understand the remaining eciency problems and how to resolve them. ..." www.seas.harvard.edu/.../Czaplicki.pdf