First Class Composable Events in F#
If you’ve been following me on Twitter lately, I’ve been playing a lot lately with event based programming in both F# events as well as the Reactive Framework (RX). Today, I’m going to start a series in event processing, starting from the elementary concept of first class events in F#.
Event-based programming is a pretty common scenario in .NET programming today, especially used in user interface programming. These events are a way of a class to notify when something of interest happens to an object, such as clicking a button on a form. Just as you would do in C#, programming with events is fairly straight forward to add and remove event handlers from an object.
> open System;; > open System.Windows.Forms;; > let form_Click (sender:obj) (e:EventArgs) = - MessageBox.Show("Clicked me!") |> ignore;; val form_Click : obj -> EventArgs -> unit > let form = new Form(Text="Click Example", TopMost=true, Visible=true);; val form : Form = System.Windows.Forms.Form, Text: Click Example > let handler = new EventHandler(form_Click);; val handler : EventHandler > form.Click.AddHandler handler;; val it : unit = () > form.Click.RemoveHandler handler;; val it : unit = ()
This simple example above illustrates both adding of an event handler for the click event as well as removing it. This behavior does not differ from what you’re used to in other languages. This in itself is interesting, but quite imperative and not at all composable. This is where the similarity ends to other .NET languages at this time.
Diving Into the IEvent
Let’s relook at a part of the example above in regards to how we declared our Click event. In F# interactive, let’s see what exactly that Click event is:
> form.Click;; val it : IEvent<EventHandler,EventArgs> = FSI_0036+it@580-21
As you can see, F# automatically turns our events into an IEvent<EventHandler, EventArgs>. What are those exactly? Well, the previous example using the AddHandler and RemoveHandler actually come from its base interface, the IDelegateEvent and the IEvent simply gives us a way to add listeners to our event:
type IDelegateEvent<'Del when 'Del :> System.Delegate > = abstract AddHandler: handler:'Del -> unit abstract RemoveHandler: handler:'Del -> unit type IEvent<'Del,'Args when 'Del : delegate<'Args,unit> and 'Del :> System.Delegate > = abstract Add: callback:('Args -> unit) -> unit inherit IDelegateEvent<'Del>
What we’re looking at is the IDelegateEvent gives us the ability to add and remove handlers much as we would in C#, but the IEvent is a bit different. This interface contains a single method, Add which takes a callback function which supplies the arguments and returns nothing, and in the case of our Click event, the arguments are System.EventArgs.
Going back to our previous example, we could have written a handler for our event such as the following:
> form.Click.Add(fun _ -> MessageBox.Show("Clicked me!") |> ignore);; val it : unit = ()
Now we have seen the basic capabilities of adding and removing event handlers with F# using standard ways that we might in other .NET languages. What makes this so different?
Composable First Class Events
Some of the ideas around these events as first class values comes to us from a language that brought us other great concepts such monads, in Haskell. This concept, called Functional Reactive Programming (FRP), integrates time flow and compositional events into a functional programming language. This gives us a compositional way of expressing our standard user interfaces but as well as simulations, animations, and so on.
As you saw above, our form.Click really is a first class value, meaning this IEvent can be passed around just as any other value. In addition, we can take advantage of the standard combinators in the Event module. Some of them are as follows:
Function | Meaning |
choose | Return a new event which fires on a selection of messages from the original event. The selection function takes an original message to an optional new message. |
create | Create an IEvent with no initial listeners. Two items are returned: a function to invoke (trigger) the event, and the event that clients can plug listeners into. |
filter | Return a new event that listens to the original event and triggers the resulting event only when the argument to the event passes the given function |
listen | Run the given function each time the given event is triggered. |
map | Return a new event that passes values transformed by the given function |
merge | Fire the output event when either of the input events fire |
pairwise | Return a new event that triggers on the second and subsequent triggerings of the input event. |
partition | Return a new event that listens to the original event and triggers the first resulting event if the application of the predicate to the event arguments returned true, and the second event if it returned false |
scan | Return a new event consisting of the results of applying the given accumulating function to successive values triggered on the input event. |
split | Return a new event that listens to the original event and triggers the first resulting event if the application of the function to the event arguments returned a Choice1Of2, and the second event if it returns a Choice2Of2 |
Many of these above functions are standard combinators that we would find on other modules such as List, Seq, Option and so on. Let’s walk through some of these combinators in a practical way. First, let’s look at map and filter:
> let form = new Form(Visible=true, TopMost=true, Text="Event Sample") - form.MouseDown - |> Event.map (fun args -> (args.X, args.Y)) - |> Event.filter (fun (x, y) -> x > 100 && y > 100) - |> Event.listen (fun (x, y) -> printfn "(%d, %d)" x y);; val form : Form = System.Windows.Forms.Form, Text: Event Sample
What this code allows us to do is first map the arguments so that we extract both the X and Y coordinate, then filter based upon the coordinates being greater than 100, and finally print out their value when the event is invoked. Just as well, I could have written that same code using the choose instead of the map and filter such as this:
> let form = new Form(Visible=true, TopMost=true, Text="Event Sample") - form.MouseDown - |> Event.choose (fun args -> - if args.X > 100 && args.Y > 100 then Some(args.X, args.Y) - else None) - |> Event.listen (fun (x, y) -> printfn "(%d, %d)" x y);; val form : Form = System.Windows.Forms.Form, Text: Event Sample
Seems rather straight forward as the choose is the ability to do both a map and a filter at the same time and return an optional value as the result. Let’s look at yet another example? How about merging events together until another operation happens such as MouseMove and MouseDown while the left button is down?
> let form = new Form(Visible=true, TopMost=true, Text="Event Sample") - form.MouseDown - |> Event.merge form.MouseMove - |> Event.filter (fun args -> args.Button = MouseButtons.Left) - |> Event.map (fun args -> (args.X, args.Y)) - |> Event.listen (fun (x, y) -> printfn "(%d, %d)" x y);; val form : Form = System.Windows.Forms.Form, Text: Event Sample
What this code does for us is that we merge the form’s MouseDown and MouseMove events together, then filter it to ensure that the left button was invoked, and finally map and listen to the event, thus printing out the mouse coordinates.
One last example we’ll cover here is to be able to partition events. This is the ability, based upon a predicate function, to split the event stream in two event streams. For example, if you wanted to handle streams differently based upon whether the amount of an amount was above a certain threshold, then we could handle both cases differently. Let’s look at a quick example of partitioning an event based upon a certain threshold:
> let form = new Form(Visible=true, TopMost=true, Text="Event Sample") - let (overEvent, underEvent) = - form.MouseMove - |> Event.merge form.MouseDown - |> Event.filter (fun args -> args.Button = MouseButtons.Left) - |> Event.map (fun args -> (args.X, args.Y)) - |> Event.partition (fun (x, y) -> x > 100 && y > 100) - overEvent |> Event.listen (fun (x, y) -> printfn "Over (%d, %d)" x y) - underEvent |> Event.listen (fun (x, y) -> printfn "Under (%d, %d)" x y);; val form : Form = System.Windows.Forms.Form, Text: Event Sample val underEvent : IEvent<int * int> val overEvent : IEvent<int * int>
What we’re doing is the basic operations much as we did above, but we’re adding an extra step to partition the single event into two based upon whether the mouse coordinates are greater than 100 on both the X and Y axis. Then we could subscribe to each event and in this case, we print out either Over and Under based upon where the mouse is. I think that’s enough to digest in one post, but there is more to come with first class events.
Conclusion
Events and event based programming is a well understood concept within the .NET space and supported by all .NET languages. F# treats events, however, as first class values can be composed in quite interesting ways as we’ve shown here. In further posts, we’ll take this further with both F# events as well as the Reactive Framework, so stay tuned.