F# First Class Events – Changes on Creating Events
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 previous post, I talked about how to manage the lifetime of a subscription. This time, I want to go over creating events again to show how it will be going forward in Visual Studio 2010 and beyond.
From create to new Event
In the previous posts in this series, in order to create the F# first class events, I used the create function in the Event module which looked like the following:
Event.create : // No arguments unit -> // Handler function and event tuple ('a -> unit) * #Event<'a>
Instead, the F# team has approached creating events in an object-oriented manner through the Event class. This gives us the ability to encapsulate how we both trigger and publish our custom events. Let’s look at the type signature for this event.
/// Event implementations for the IEvent<_> type type Event<'T> = /// Create an event object suitable for /// implementing for the IEvent<_> type new : unit -> Event<'T> /// Trigger the event using the given parameters member Trigger : arg:'T -> unit /// Publish the event as a first class event value member Publish : IEvent<'T>
There is also an overloaded version of this class which is specifically there for support of .NET events. This version follows the standard sender and event arguments signature that we are used to when dealing with events. The type signature for that class like the following:
/// Event implementations for a delegate types following /// the standard .NET Framework convention of a first /// 'sender' argument type Event<'Del,'Args when 'Del : delegate<'Args,unit> and 'Del :> System.Delegate > = /// Create an event object suitable for delegate types /// following the standard .NET Framework convention of /// a first 'sender' argument new : unit -> Event<'Del,'Args> /// Trigger the event using the given sender object /// and parameters. /// The sender object may be <c>null</c>. member Trigger : sender:obj * args:'Args -> unit /// Publish the event as a first class event value member Publish : IEvent<'Del,'Args>
Knowing this, we can create our custom events much like before by creating a new Event<_> instance and then publishing. Below is a quick example of doing exactly that:
> let event = new Event<int>() - let iEvent = event.Publish;; val event : Event<int> val iEvent : IEvent<int> > iEvent.Add(printfn "Triggered with %d");; val it : unit = () > event.Trigger(3);; Triggered with 3 val it : unit = () > event.Trigger(5);; Triggered with 5 val it : unit = ()
What we notice from the above code is that we create the Event class with a type (or let type inference do the trick), and then publish it to create the IEvent instance in which we can subscribe, map, filter and so on. In order to invoke our custom event, we simply call the Trigger method on the Event class. Let’s look at another example, this time for splitting an event into three pieces, and in this case a WebClient for downloading a string asynchronously and then having events for whether success, an exception and a cancellation.
First, let’s define our split3 function:
module Event = let split3 (splitter : 'T -> Choice<'U1,'U2,'U3>) (event: IEvent<'Del,'T>) = let ev1 = new Event<_>() let ev2 = new Event<_>() let ev3 = new Event<_>() event.Add(splitter >> function | Choice1Of3 x -> ev1.Trigger(x) | Choice2Of3 x -> ev2.Trigger(x) | Choice3Of3 x -> ev3.Trigger(x)) (ev1.Publish, ev2.Publish, ev3.Publish)
In this instance, I created three events, and trigger each based upon the outcome of our incoming splitter function. The splitter function takes our argument and returns a choice of 3 values. Note that this is different from partition as our choice values can contain any data type that we want. Now, let’s look at splitting up the WebClient’s DownloadStringCompleted event into our three new events:
open System.Net let client = new WebClient() let success, failure, cancel = client.DownloadStringCompleted |> Event.split3 (fun args -> if args.Cancelled then Choice3Of3() elif args.Error <> null then Choice2Of3(args.Error) else Choice1Of3(args.Result)) success.Add(fun result -> printfn "Received %d characters" result.Length) failure.Add(fun exn -> printfn "Exception occurred: %s" exn.Message) cancel.Add(fun () -> printfn "Cancelled!")
What I did above was to split the DownloadStringCompleted event into three, one for success, one for exception handling and one for cancellation. If we have an exception, we return the second choice with the exception, else if there is a cancellation, then we return our third choice with nothing, and if we have a result then we return our first choice with the data. After that, I added listeners to each event acting appropriately.
Let’s try two quick examples, one for throwing an exception and one for success:
> open System;; > client.DownloadStringAsync(Uri "http://cnn.com");; val it : unit = () Received 101113 characters > client.DownloadStringAsync(Uri "http://cnn.com") - client.CancelAsync();; Cancelled! > client.DownloadStringAsync(Uri "http://foo.bar");; val it : unit = () Exception occurred: The remote server returned an error: (400) Bad Request.
As you’ll notice from the above behavior is that on success, we get the number of characters, on a cancel, we get our cancel message, and lastly on our bad URL, we get an exception returned of a bad request.
Conclusion
Going forward, creating the Event class is the way to both create, publish and trigger events. Unlike other .NET languages, instead of exposing our events as members, we can create our new Event class instances which gives us a bit more interesting power (instead of the Event.create function). We’re not quite finished here as we have a lot more to cover with both first-class events in F#, especially with Async Workflows, as well as the Reactive Framework.