F# infoof operator (part 3)
With properties and methods covered, we can turn to events. The eventof helper returns a System.Reflection.EventInfo from an event access expression. First, we need to understand what an event access expression means in F#. A CLI event is represented by its add_EventName and remove_EventName methods, together with metadata that associates them:
class Foo
{
public event EventHandler Completed;
}
In C#, code outside Foo can use this event only to subscribe or unsubscribe a handler:
var foo = new Foo();
foo.Completed += SomeMethodName;
foo.Completed -= SomeMethodName;
F# supports first-class events, so an event can be represented as a value and used like any other value. For example, this F# type defines two events: a CLI-compatible event named Bar and an ordinary F# event named Baz:
type Foo() =
let bar = Event<int>()
let baz = Event<int>()
[<CLIEvent>]
member __.Bar = bar.Publish
member __.Baz = baz.Publish
Given an instance named foo, we can put both events in a list and subscribe to them in a loop:
for e in [ foo.Bar; foo.Baz ] do
e.AddHandler(fun _ _ -> printfn "!")
What do the expressions foo.Bar and foo.Baz represent? Their quotations show the difference:
let foo = Foo()
<@ foo.Bar @>
Call (None,
IEvent`2[...] CreateEvent[FSharpHandler`1,Int32](...),
[Lambda (eventDelegate,
Call (Some foo,
Void add_Bar(FSharpHandler`1[Int32]),
[eventDelegate])),
Lambda (eventDelegate,
Call (Some foo,
Void remove_Bar(FSharpHandler`1[Int32]),
[eventDelegate])),
Lambda (callback,
NewDelegate (FSharpHandler`1[Int32],
[ a1; a2 ],
Application (
Application (callback, a1), a2)))])
<@ foo.Baz @>
PropertyGet (Some foo,
IEvent`2[FSharpHandler`1[Int32],Int32] Baz, [])
An ordinary F# event such as Baz is exposed as a CLI property returning an IEvent object. For a CLI event such as Bar, however, F# generates a call to the standard library’s internal CreateEvent helper at the point of access. This helper constructs an IEvent from functions that subscribe and unsubscribe handlers, together with a function that creates the appropriate delegate. F# can therefore present both forms uniformly as first-class IEvent values, while the compiler handles the wrapping of the CLI event’s add and remove accessors.
Returning to eventof, only CLI-compatible events have corresponding EventInfo metadata. We need to extract their accessors from the generated wrapper and use that information to find the event on the declaring type:
let eventof expr =
match expr with
| Call(None, createEvent, [
Lambda(arg1, Call(_, addHandler, [ Var var1 ]))
Lambda(arg2, Call(_, removeHandler, [ Var var2 ]))
Lambda(_, NewDelegate _)
])
when createEvent.Name = "CreateEvent"
&& addHandler.Name.StartsWith("add_")
&& removeHandler.Name.StartsWith("remove_")
&& arg1 = var1
&& arg2 = var2 ->
addHandler.DeclaringType.GetEvent(
addHandler.Name.Remove(0, 4), // event name
BindingFlags.Public ||| BindingFlags.Instance |||
BindingFlags.Static ||| BindingFlags.NonPublic)
| _ -> failwith "Not an event expression"
The pattern looks for a call named CreateEvent with three lambda arguments. The first two must call methods whose names begin with add_ and remove_, passing their respective lambda parameters as the handler arguments. The event name is then obtained by removing the add_ prefix, and GetEvent looks it up on the type that declares the add accessor. For example:
eventof<@ foo.Bar @>
val it : EventInfo =
FSharpHandler`1[System.Int32] Bar
{Attributes = None;
DeclaringType = Foo;
EventHandlerType = FSharpHandler`1[System.Int32];
IsMulticast = true;
MemberType = Event;
Name = "Bar";
...}
In the final post, we will bring these helpers together into the complete MemberInfo module, adding support for fields, constructors, and discriminated union cases.