次の方法で共有


F# First Class Events: Simplicity and Compositionality in Imperative Reactive Programming

The text of this post is also available as an article , which I'll modify with latest material on this topic as time goes on.

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# language, 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. that 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 being 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.)

Note 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 know if or how the event handlers are stored or triggered. IDelegateEvent (see below) also supports 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 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 Exceptions, 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 a 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. 

IDelegateEvent and the connection to .NET events

Finally, a word on IDelegateEvent and removing handlers. IDelegateEvent is a subtype refinement of IEvent primarily used by the F# compiler, though you will see it as part of IntelliSense or if you define your own event types.  It corresponds 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 an 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# language 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# language design converge to allow a powerful approach to wiring based on event composition, transformation and selection. 

We hope you enjoy programming with F# events!

The F# team, 24/03/2006

Comments

  • Anonymous
    March 24, 2006
    When will be available first release for commercial use? :)
    I'm still waiting with testing F# until it will be available for commercial use...

  • Anonymous
    April 05, 2006
    Hi Mateusz,

    Programs you develop and compile with F# can already be used for commercial purposes (See the license for details...)  

    BTW there is a comment on the MSR downloads page that falsely implies that all downloads are non-commercial-use only.  The F# download is made available under less stringent conditions.

    Cheers!
    Don

  • Anonymous
    March 17, 2007
    PingBack from http://www.markstaples.com/2006/06/05/f-sample-events-and-net-firebird/

  • Anonymous
    June 08, 2009
    PingBack from http://jointpainreliefs.info/story.php?id=693