F# provides first-class events, but I found a few limitations in their implementation that are worth examining.

To expose events on F# types to subscribers written in other CLI languages, the standard approach uses DelegateEvent, which invokes its handlers through reflection. Vladimir Matveev describes the resulting performance problem in “F# performance of events” and its update. The difficulty is invoking a delegate from generic code when its type is itself a type parameter. Vladimir proposed solutions using code generation or F# member constraints, but there is a simpler approach with less overhead. It turned out that he had also discovered this approach before I did.

Delegate.CreateDelegate can create an open instance delegate for a delegate type’s Invoke() method. The first argument to the resulting delegate supplies the receiver that would normally be passed as this. This gives us a typed invoker for instances of the chosen delegate type, without generating code or duplicating the bodies of inline methods as the member-constraint approach can do.

Another limitation is that the F# standard library’s Event and DelegateEvent classes do not synchronize subscription and unsubscription. By comparison, before C# 4.0, the C# compiler synchronized the accessors generated for field-like events using lock(this) for instance events and lock(typeof(ContainingType)) for static events. Starting with C# 4.0, it generates lock-free subscription code instead. Both approaches can also be implemented in F#.

@cadet354 also suggested supporting asynchronous invocation, for cases where the caller does not need to wait for handlers to finish, and parallel invocation using F# async workflows. These options may be useful even if they are less common than synchronous event delivery.

The following prototype combines these invocation and subscription options:

open System
open System.Threading

[<Sealed>]
type PowerEvent<'del, 'args
     when 'del :  not struct             // reference type
      and 'del :  delegate<'args, unit>  // delegate signature
      and 'del :> Delegate               // derives from System.Delegate
      and 'del :  null>() =              // supports null

  [<DefaultValue>]
  val mutable private target : 'del

  // create an invoker for delegates of type 'del
  static let invoker : Action<_,_,_> =
    downcast Delegate.CreateDelegate(
      typeof<Action<'del, obj, 'args>>, typeof<'del>.GetMethod "Invoke")

  // invoke the event handlers synchronously
  member self.Trigger (sender: obj, args: 'args) =
     match self.target with
     null    -> ()
   | handler -> invoker.Invoke (handler, sender, args)

  // invoke the event handlers asynchronously
  member self.TriggerAsync (sender: obj, args: 'args) =
     match self.target with
     null    -> ()
   | handler ->
         async { invoker.Invoke (handler, sender, args) }
         |> Async.Start

  // invoke the event handlers asynchronously, allowing parallel execution
  member self.TriggerParallel (sender: obj, args: 'args) =
     match self.target with
     null    -> ()
   | handler ->
         handler.GetInvocationList ()
      |> Array.map (fun h -> async {
           invoker.Invoke (downcast h, sender, args)
         })
      |> Async.Parallel
      |> Async.Ignore
      |> Async.Start

  // to avoid creating an IDelegateEvent<'del> wrapper
  // for every subscription or unsubscription, implement
  // the unsynchronized interface directly here:
  interface IDelegateEvent<'del> with

     member self.AddHandler handler =
       self.target <- downcast Delegate.Combine (self.target, handler)

     member self.RemoveHandler handler =
       self.target <- downcast Delegate.Remove (self.target, handler)

  // expose the event without synchronizing subscription changes
  member self.Publish = self :> IDelegateEvent<'del>

  // expose the event with lock-based subscription changes
  member self.PublishSync =
   { new IDelegateEvent<'del> with

     member __.AddHandler handler =
       lock self (fun() ->
            self.target <- downcast Delegate.Combine (self.target, handler))

     member __.RemoveHandler handler =
       lock self (fun() ->
            self.target <- downcast Delegate.Remove (self.target, handler)) }

  // expose the event with lock-free
  // synchronization of subscription changes
  member self.PublishLockFree =
   { new IDelegateEvent<'del> with

     member __.AddHandler handler =
       let rec loop o =
         let c = downcast Delegate.Combine (o, handler)
         let r = Interlocked.CompareExchange(&self.target,c,o)
         if obj.ReferenceEquals (r, o) = false then loop r
       loop self.target

     member __.RemoveHandler handler =
       let rec loop o =
         let c = downcast Delegate.Remove (o, handler)
         let r = Interlocked.CompareExchange(&self.target,c,o)
         if obj.ReferenceEquals (r, o) = false then loop r
       loop self.target }

The class offers three ways to manage subscriptions:

  • Without synchronization
  • With locking, as in C# versions before 4.0
  • With lock-free synchronization, as in C# 4.0

It also provides three ways to invoke the handlers:

  • Synchronously: invoke handlers in sequence and wait for them to finish.
  • Asynchronously: invoke handlers in sequence on another thread, without waiting for them to finish.
  • Asynchronously with parallel execution where possible, without waiting for the handlers to finish.

Usage is similar to ordinary F# events:

type Foo() =
    let event = PowerEvent<EventHandler, _>()

    member self.Fire() = event.Trigger (self, EventArgs.Empty)

    [<CLIEvent>] member this.Event1 = event.Publish
    [<CLIEvent>] member this.Event2 = event.PublishSync
    [<CLIEvent>] member this.Event3 = event.PublishLockFree

This is a prototype and has not been thoroughly tested.