F# First Class Events – Async Workflows + Events Part I

So far in this series, I’ve covered a bit about what first class events are in F# and how you might use them.  In the first post, we looked at what a first class events mean and some basic combinators in order to compose events together.  In the second post, we looked at how we might create events and publish them to the world through classes.  And in the third post I talked about how to manage the lifetime of a subscription.  Last time, I corrected my usage of the old create function and instead to use the Event class to create, trigger and publish events.  This time, we’ll look at how we can use first class events inside Async Workflows in order to do such items as tracking state.

Awaiting Events

You may have noticed me talk a bit about asynchronous programming and event-based programming both in F#, but what about bringing the power of both of them together?  What if we could somehow interact with first class events inside of async workflows to wait for an event and then continue when that event happens?  Inside the F# PowerPack, there is an extension method to do exactly that.  The AwaitEvent extension method to the Async<T> class takes an event and an optional cancellation action and returns an async binding for our event so that we can wait for a single invocation of the event and then use its event arguments.  Let’s look at the signature below.

type Microsoft.FSharp.Control.Async<'T> with 
  static member AwaitEvent: 
    IEvent<'Delegate,'T> * ?cancelAction : (unit -> unit) -> Async<'T> 
      when 'Delegate : delegate<'T,unit> and 'Delegate :> System.Delegate 

What you’ll notice is that this returns an asynchronous computation and takes the event, which it listens to once and then disposes once it is completed or cancelled.  If the cancellation action is specified and a cancellation occurs, then the function is executed and will continue to wait for the event, however, if not specified, then the computation is immediately halted.

Typically, when we’re talking about aggregating data as I’ve stated above, we might consider the scan combinator from the Event module.  This allows us, with a given seed, to aggregate data as it comes in.  Given a quick use of a WPF window, it might look like the following:

open System.Windows

let window = new Window(Visibility = Visibility.Visible)
window.MouseDown
  |> Event.scan (fun acc args -> args.ClickCount + acc) 0
  |> Event.listen (printfn "Clicked %d time(s)")

This above code allows us to count the number of times a mouse is clicked and display that on F# interactive.  Pretty simple code, but how might we handle cancellation or even how to stop listening for this?  Well, given this example, not so easily.  So, how can we use async workflows instead?  Let’s try the above example using the new technique:

open System.Windows

let window = new Window(Visibility = Visibility.Visible)

let trackClicks (e : #UIElement) = 
  let rec loop (n:int) =
    async { let! args = Async.AwaitEvent e.MouseDown
            let clickCount = args.ClickCount + n
            printfn "Clicked %d time(s)" clickCount
            return! loop clickCount }
  loop 0

trackClicks window |> Async.Start

What this code does is first create a WPF window which we’ll use later for tracking the mouse clicks.  Next, we define a function called trackClicks which takes in an a UIElement for which to track mouse clicks.  Inside, we have a loop, because we remember that these event waiting functions only wait one time in which we seed the click count to zero.  Inside of our loop function, we get the arguments from our MouseDown event so that we can get the number of clicks given to us, we increment the click count, print it, and then return to the loop again so that we keep listening for click events.  Finally, we start listening by calling our trackClicks function with our window argument and then starting it asynchronously.  The output from our little application might look like this from F# Interactive just as it should have for the above example:

Clicked 1 time(s)
Clicked 3 time(s)
Clicked 6 time(s)
Clicked 7 time(s)
Clicked 8 time(s)
Clicked 10 time(s)
Clicked 13 time(s)
Clicked 17 time(s)
Clicked 18 time(s)
Clicked 20 time(s)

We get the same exact behavior as we would above, but yet gives us some more power over such things as cancellation, alternate paths, etc. 

Conclusion

From this naive example, we’re able to see that asynchronous workflows and first class events can indeed work nicely together.  In the next couple of posts, we’ll go deeper into this subject to explore such areas as drawing on a screen as well as downloading data from a site asynchronously.  After this, we’re not quite finished here as we have a lot more to cover with event-based programming with such things as the Reactive Framework (Reactive LINQ).

No Comments