F# Events, Reactive Programming and Async Workflows

This is my contribution to the 2015 F# Advent Calendar. I hope you like it!

The majority of my code-slinging career has focused on full stack web development which hasn't necessitated a lot of complex event-driven programming, unless you include the unholy event model in Web Forms. Even in the cases where I do need to work with events, it's usually abstracted behind a library or framework (shout-out to Knockout). Working with events has become much more relevant now that I'm doing more mobile development. Fortunately F# has some awesome language features that make working with events a lot of fun. Before we jump into events though we're going to look briefly at async programming in F#. Then we'll look at the F# programming model for events. Finally, we'll bring it all together in a comprehensive, if a little contrived, example.

F# Async Workflows

For whatever reason, F# async workflows have been one of the hardest things for me to grok. The concepts started to click when I read this excellent write-up on the subject.

The goal of asynchronous programming is to start expensive and/or time consuming processes on a separate thread and continue doing work on the main thread until we have to wait for the expensive task to finish. It's kind of like multitasking except that asynchronous programming actually is more efficient. ;)

The following code sample shows a couple of expensive tasks running in sequence. We can't do anything on the main thread until they complete.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
open System

let timer1 = new Timers.Timer(500.)
let timer2 = new Timers.Timer(250.)

timer1.Elapsed.Add
    (fun x ->
        printfn "Timer 1: %d:%d" x.SignalTime.Second x.SignalTime.Millisecond)
timer2.Elapsed.Add
    (fun x ->
        printfn "Timer 2: %d:%d" x.SignalTime.Second x.SignalTime.Millisecond)

let runTimer (t : System.Timers.Timer) (s : int) =
    t.Start()
    System.Threading.Thread.Sleep s
    t.Stop()

//Run timers sequentially
runTimer timer1 2500
runTimer timer2 2500

While the above code is contrived, it's not hard to imagine each timer representing an HTTP request to a web API to get data that our app needs before it can be useful. As it stands we have to wait 5 seconds for these tasks to finish before the app startup completes.

Now here's the same code using F# async workflows:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
open System

let timer1 = new Timers.Timer(500.)
let timer2 = new Timers.Timer(250.)

timer1.Elapsed.Add
    (fun x ->
        printfn "Timer 1: %d:%d" x.SignalTime.Second x.SignalTime.Millisecond)
timer2.Elapsed.Add
    (fun x ->
        printfn "Timer 2: %d:%d" x.SignalTime.Second x.SignalTime.Millisecond)

// new stuff starts here
let runTimerAsync (t : System.Timers.Timer) s =
    async {
        t.Start()
        // Async workflow needs to do something (basically block here)
        do! Async.Sleep s
        t.Stop()        
    }

// Run timers in parallel
Async.Parallel [(runTimerAsync timer1 5000); (runTimerAsync timer2 5000)]
|> Async.RunSynchronously

Thanks to async workflows, we've cut the amount of time it takes to regain control of the main thread nearly in half. But what if we could fire off each request independently and let the async workflow notify us when it's done? That's exactly what the following example does:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
Async.StartWithContinuations(
    runTimerAsync timer1 2500,
    (fun _ -> printfn "Timer 1 finished"),
    (fun _ -> printfn "Timer 1 threw an exception"),
    (fun _ -> printfn "Cancelled Timer 1"))

Async.StartWithContinuations(
    runTimerAsync timer2 1500,
    (fun _ -> printfn "Timer 2 finished"),
    (fun _ -> printfn "Timer 2 threw an exception"),
    (fun _ -> printfn "Cancelled Timer 2"))

printfn "Some other task that isn't blocked"

// Output:
// "Some other task that isn't blocked"
// timer output...
// "Timer 2 finished"
// "Timer 1 finished"

In the example above, the list of async workflows is broken into two Async.StartWithContinuations calls. Each runs independently somewhere in the background and is given a function to execute when the task is done. Each async call is non-blocking, which means work can continue on the main thread until the expensive task completes.

F# Events

F# events are straightforward to work with. When working with existing events, you can add a handler to the event using the Event.Add method. Referring back to the timer instances from the previous example, we were able to add handlers for the Elapsed event with lambdas (anonymous functions) like this: timer.Elapsed.Add (fun x -> printfn "%A" x).

Adding a custom event to a type (class) is also simple. Let's create a type that determines whether or not a number is prime and fires an event when called. I chose prime numbers because they come up a lot in programming exercises, and it's interesting see the different ways to determine if a number is prime efficiently.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
// Type for testing primality of numbers
// Include custom event that triggers when helper is called.
type PrimeFinder() =
    let primeFoundEvent = Event<_>()

    member x.PrimeFound = primeFoundEvent.Publish
    member x.IsPrime n =
        primeFoundEvent.Trigger(x, (n, isPrime n))

let primeFinder = PrimeFinder()
primeFinder.PrimeFound.Add (fun (_, e) ->
    match e with
    | n, true -> printfn "%d is prime" n
    | n, false -> printfn "%d is not prime" n)

primeFinder.IsPrime 3L // prints "3 is prime"
// PrimeFound event should not fire
primeFinder.IsPrime 4L // prints "4 is not prime"

PrimeFinder is a type that exposes a single method, IsPrime, and an event, PrimeFound, that we can add handlers to. primeFoundEvent is a local value of type Event<_>. Notice that the type passed to Event is not defined explicitly. By using the wildcard "_" syntax we're telling the compiler to figure out the event type based on its usage. PrimeFinder exposes primeFoundEvent as PrimeFound via the Publish method, which returns an object of type IEvent<PrimeFinder * obj>. Finally, IsPrime calls the Trigger method on primeFoundEvent. The arguments passed to Trigger are the self-identifier for the type, x, and a tuple of type int64 * bool. Said another way, when we call the Trigger method we're passing it the sender and eventArgs.

Note that when IsPrime calls primeFoundEvent.Trigger with arguments of type int64 * bool, the F# compiler infers PrimeFound's type to be IEvent<PrimeFinder *(int64 * bool)>.

One of the neat things about F# events is that you can treat them as event streams (like with Rx programming). An event stream is essentially a collection of events that you can apply function calls like map, reduce, and filter to.

So maybe we only want the event to call our handler when a number is prime. We could easily update our handler to print only when a number is prime, but what if we could separate out the responsibility of handling the event from determining whether or not we should handle the event?

The following sample creates an event stream from PrimeFound and returns only events whose arguments are prime. The previous handler is then added to the filtered stream. The logic for determining when to handle an event is now separate from the logic describing how to handle an event.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
let primeFinder = PrimeFinder()

primeFinder.PrimeFound
    |> Event.filter (fun (_, e) -> snd e) // only where the number was prime
    |> Event.add (fun (_, e) ->
        match e with
        | n, true -> printfn "%d is prime" n
        | n, false -> printfn "%d is not prime" n)

// PrimeFound handler should fire.
primeFinder.IsPrime 3L
// PrimeFound handler should not fire
primeFinder.IsPrime 4L

What's really nice about this approach is that we can change our filtering logic at any time without touching the event handler. PrimeFound can be treated as a stream by virtue of being of type IEvent thanks to primeFoundEvent.Publish.

More Events

Now let's say we want to start a long running process in the background and we want to be able to check on the status of it as we go. The code for the background process might look something like:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
let findPrimesAsync numbers primeTester = 
    let rec findPrimes n = async {
        match n with
        | h::t ->
            do! Async.SwitchToNewThread()
            primeTester h
            do! Async.SwitchToThreadPool()
            do! findPrimes t
        | [] -> ()
        
    }
    findPrimes number

findPrimesAsync takes a list of numbers and a function that determines the primality of a number and then applies that function to each number in an async workflow. There are a lot of ways to iterate over the collection and apply the primeTester function to each item, but I went with an F# list to see what it looked like being solved recursively. Note that the first time you call the inner recurisve function findPrimes you can't use the do! binding because you're outside the async workflow. (This is probably obvious to most people but it wasn't to me. Fortunately the compiler will tell you.)

At this point we could declare a mutable binding (boo), fire off Async.Start on findPrimesAsync and use PrimeFinder to set the mutable binding whenever a prime is found. We could then access the mutable binding later. However, the best way to get comfortable with F# is to write it idiomatically so let's see how we can tackle this functionally, and without mutable state.

We already know we can wire a handler that will get called whenever a prime is found. It would be nice if there were an event whose argument was the most recent prime number to be processed...

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
// Record type give our event args some structure
type PrimeTime =
    {Time:DateTime; Prime:int64 option; ID:string}
    override x.ToString() =
        sprintf "%d - %s" 
            (match x.Prime with | Some(e) -> e | None -> 0L)
            (x.Time.ToLongTimeString())
            
let primeFinder = PrimeFinder()
let pFound = 
    primeFinder.PrimeFound
    |> Event.filter (fun (_, e) -> snd e) // only where the number was prime
    |> Event.map (fun (_, e) -> 
        {Time = DateTime.Now; Prime = Some(fst e); ID = "p"})

The value pFound only includes events where the PrimeFound returned true. The event argument is then mapped to the record type PrimeTime. This record type provides extra context about the event, such as when the event fired, where the event originated from, and what the event arguments were.

For the sake of this exercise, we'll check the status of the list processing with a button click. When the button is clicked, we want to see the most recent prime number found in the collection. The following code takes the click event from a button and maps it onto an Event of type PrimeTime, just like pFound!

1: 
2: 
3: 
4: 
5: 
let checkStatusButton = new Button(Text = "Check Status")
let csClicked =
    checkStatusButton.Click 
    |> Event.map (fun _ ->
        {Time = DateTime.Now; Prime = None; ID = "c"})

pFound and csClicked are both of type IEvent<PrimeTime>. This is ultimately the type needed for the final event stream. Getting there is a little bit interesting though.

csClicked will never have a value for Prime. However, pFound will always have a value for Prime. What we need is a stream containing the latest event emitted by both pFound and csClicked.

The following snippet uses Event.merge to combine the streams pFound and csClicked. Event.scan then reduces the merged stream into a stream of type IEvent<PrimeTime option * PrimeTime option>. The first PrimeTime option in the tuple is either Some or None from the pFound stream. The second PrimeTime option in the tuple is either Some or None from the csClicked stream. Each time either pFound or csClicked emits an event, Event.scan passes the tuple and newest event through an aggregation function. When a pFound event passes through the aggregate function a tuple of (Some(e), None) is returned. When a csClicked event passes through the aggregate function, a tuple of (p, Some(e)) is returned. Using this pattern, the aggregate function will always return the latest PrimeFound event (if any) when e is a Click event, but will not return a Click event with when e is PrimeFound event. Doing this guarantees that when a tuple with a Click event is returned, it will have the most recent prime number found if there is one.

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
let reduced =
    pFound
    |> Event.merge csClicked
    |> Event.scan (fun (p, c) e ->
        match e.ID with
        |"p" -> (Some(e), None)
        |"c" -> (p, Some(e))
        |_ -> (p, c)) (None, None)

With reduced in hand, we can now get the most recent prime number found when the "Check Status" button is clicked. This new event stream should only emit events when the "Check Status" button is clicked. This means we only want events from the reduced stream where the second value in the tuple is Some. After the stream is filtered to make sure that a "Check Status" click occurred, we map the event stream back into an IEvent<PrimeTime> stream. The objective of this map operation is updating the None value on the button click event with the Some value from the pFound event if there is one. If the pFound stream hasn't emitted an event we return the unchanged csClicked event. If a pFound event has occurred, the prime number found from that event is swapped in for the None emitted by the csClicked event.

Below is the full code for the filtered, mapped, and refiltered statusCheck stream.

1: 
2: 
3: 
4: 
5: 
6: 
7: 
let statusCheck =
    reduced
    |> Event.filter (fun (p, c) -> c.IsSome)
    |> Event.map (fun (p, c) ->
        match p with
        | Some f -> { c.Value with Prime = f.Prime }
        | None -> c.Value)

With an async workflow defined for our background process and an event stream we can listen to to get the last prime found we're ready to bring it all together. Below is the full code for this example. Try running it in F# interactive. Clicking the "Check Status" button will write either the latest prime found or 0 (zero) to FSI. Clicking "Cancel" will kill the async workflow.

  1: 
  2: 
  3: 
  4: 
  5: 
  6: 
  7: 
  8: 
  9: 
 10: 
 11: 
 12: 
 13: 
 14: 
 15: 
 16: 
 17: 
 18: 
 19: 
 20: 
 21: 
 22: 
 23: 
 24: 
 25: 
 26: 
 27: 
 28: 
 29: 
 30: 
 31: 
 32: 
 33: 
 34: 
 35: 
 36: 
 37: 
 38: 
 39: 
 40: 
 41: 
 42: 
 43: 
 44: 
 45: 
 46: 
 47: 
 48: 
 49: 
 50: 
 51: 
 52: 
 53: 
 54: 
 55: 
 56: 
 57: 
 58: 
 59: 
 60: 
 61: 
 62: 
 63: 
 64: 
 65: 
 66: 
 67: 
 68: 
 69: 
 70: 
 71: 
 72: 
 73: 
 74: 
 75: 
 76: 
 77: 
 78: 
 79: 
 80: 
 81: 
 82: 
 83: 
 84: 
 85: 
 86: 
 87: 
 88: 
 89: 
 90: 
 91: 
 92: 
 93: 
 94: 
 95: 
 96: 
 97: 
 98: 
 99: 
100: 
101: 
102: 
103: 
104: 
open System
open System.Windows.Forms

let isPrime x =
    let rec check i =
        double i > sqrt (double x) || (x % i <> 0L && check (i + 1L))
    match x with
    | y when y < 2L -> false
    | _ -> check 2L

type PrimeFinder() =
    let primeFoundEvent = Event<_>()
    
    member x.PrimeFound = primeFoundEvent.Publish
    member x.IsPrime n =
        primeFoundEvent.Trigger(x, (n, isPrime n))

type PrimeTime =
    {Time:DateTime; Prime:int64 option; ID:string}
    override x.ToString() =
        sprintf "[%s] %d - %s"
            x.ID
            (match x.Prime with | Some(e) -> e | None -> 0L)
            (x.Time.ToLongTimeString())

let findPrimesAsync numbers primeTester = 
    let rec findPrimes n = async {
        match n with
        | h::t ->
            do! Async.SwitchToNewThread()
            primeTester h
            do! Async.SwitchToThreadPool()
            do! findPrimes t
        | [] -> ()
        
    }
    findPrimes numbers

let runForm () =
    let panel = new FlowLayoutPanel()
    panel.Dock <- DockStyle.Fill
    panel.WrapContents <- false
    
    let form = new Form(Width = 400, Height = 300, Visible = true, Text = "Test")
    form.Controls.Add panel

    let startButton = new Button(Text = "Start")
    let cancelButton = new Button(Text = "Cancel")
    let checkStatusButton = new Button(Text = "CheckStatus")

    let primeFinder = PrimeFinder()

    // Transform the event args 
    // from the button.Click
    // and PrimeFound events
    // into uniform structures
    // that can be merged
    let csClicked = 
        checkStatusButton.Click 
        |> Event.map (fun _ -> {Time = DateTime.Now; Prime = None; ID = "c"})

    let pFound = 
        primeFinder.PrimeFound
        |> Event.filter (fun (_, e) -> snd e) // only where the number was prime
        |> Event.map (fun (_, e) -> 
            {Time = DateTime.Now; Prime = Some(fst e); ID = "p"})

    let reduced =
            pFound
            |> Event.merge csClicked
            |> Event.scan (fun (p, c) e ->
                match e.ID with
                |"p" -> (Some(e), None)
                |"c" -> (p, Some(e))
                |_ -> (p, c)) (None, None)

    let statusCheck =
        reduced
        |> Event.filter (fun (p, c) -> c.IsSome)
        |> Event.map (fun (p, c) ->
            match p with
            | Some f -> { c.Value with Prime = f.Prime }
            | None -> c.Value)
    
    statusCheck.Add (fun x -> printfn "Last prime: %s" <| x.ToString())
    
    let numbers = [1L..100000L]
    startButton.Click.Add (fun _ ->
        printfn "Started"
        Async.StartWithContinuations(
            findPrimesAsync numbers primeFinder.IsPrime,
            (fun _ -> printfn "Done"),
            (fun x -> printfn "Exception occurred: %A" x),
            (fun _ -> printfn "Stopped")
        )
    )

    cancelButton.Click.Add (fun _ -> Async.CancelDefaultToken())

    panel.Controls.AddRange [|startButton;cancelButton;checkStatusButton|]
        
    Application.Run(form)

runForm()

Conclusion

The basics of F# events are simple but powerful. F# events are of type Event and implement IEvent which in turn implements IObservable and IDelegateEvent. Because of this, IEvent can be mapped, filtered, and more, which allows consumers to add handlers for highly specialized workflows. We can start expensive tasks asynchronously and use events to check on the status of the work.

Resources

  1. http://fsharpforfunandprofit.com/posts/concurrency-async-and-parallel/
  2. http://dotnet.readthedocs.org/en/latest/async/async-fsharp.html
  3. http://fsharpforfunandprofit.com/posts/concurrency-reactive/
namespace System
val timer1 : Timers.Timer

Full name: fsharpeventsrxasync.timer1
namespace System.Timers
Multiple items
type Timer =
  inherit Component
  new : unit -> Timer + 1 overload
  member AutoReset : bool with get, set
  member BeginInit : unit -> unit
  member Close : unit -> unit
  member Enabled : bool with get, set
  member EndInit : unit -> unit
  member Interval : float with get, set
  member Site : ISite with get, set
  member Start : unit -> unit
  member Stop : unit -> unit
  ...

Full name: System.Timers.Timer

--------------------
Timers.Timer() : unit
Timers.Timer(interval: float) : unit
val timer2 : Timers.Timer

Full name: fsharpeventsrxasync.timer2
event Timers.Timer.Elapsed: IEvent<Timers.ElapsedEventHandler,Timers.ElapsedEventArgs>
member IObservable.Add : callback:('T -> unit) -> unit
val x : Timers.ElapsedEventArgs
val printfn : format:Printf.TextWriterFormat<'T> -> 'T

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.printfn
property Timers.ElapsedEventArgs.SignalTime: DateTime
property DateTime.Second: int
property DateTime.Millisecond: int
val runTimer : t:Timers.Timer -> s:int -> unit

Full name: fsharpeventsrxasync.runTimer
val t : Timers.Timer
val s : int
Multiple items
val int : value:'T -> int (requires member op_Explicit)

Full name: Microsoft.FSharp.Core.Operators.int

--------------------
type int = int32

Full name: Microsoft.FSharp.Core.int

--------------------
type int<'Measure> = int

Full name: Microsoft.FSharp.Core.int<_>
Timers.Timer.Start() : unit
namespace System.Threading
Multiple items
type Thread =
  inherit CriticalFinalizerObject
  new : start:ThreadStart -> Thread + 3 overloads
  member Abort : unit -> unit + 1 overload
  member ApartmentState : ApartmentState with get, set
  member CurrentCulture : CultureInfo with get, set
  member CurrentUICulture : CultureInfo with get, set
  member DisableComObjectEagerCleanup : unit -> unit
  member ExecutionContext : ExecutionContext
  member GetApartmentState : unit -> ApartmentState
  member GetCompressedStack : unit -> CompressedStack
  member GetHashCode : unit -> int
  ...

Full name: System.Threading.Thread

--------------------
Threading.Thread(start: Threading.ThreadStart) : unit
Threading.Thread(start: Threading.ParameterizedThreadStart) : unit
Threading.Thread(start: Threading.ThreadStart, maxStackSize: int) : unit
Threading.Thread(start: Threading.ParameterizedThreadStart, maxStackSize: int) : unit
Threading.Thread.Sleep(timeout: TimeSpan) : unit
Threading.Thread.Sleep(millisecondsTimeout: int) : unit
Timers.Timer.Stop() : unit
val runTimerAsync : t:Timers.Timer -> s:int -> Async<unit>

Full name: fsharpeventsrxasync.runTimerAsync
val async : AsyncBuilder

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.async
Multiple items
type Async
static member AsBeginEnd : computation:('Arg -> Async<'T>) -> ('Arg * AsyncCallback * obj -> IAsyncResult) * (IAsyncResult -> 'T) * (IAsyncResult -> unit)
static member AwaitEvent : event:IEvent<'Del,'T> * ?cancelAction:(unit -> unit) -> Async<'T> (requires delegate and 'Del :> Delegate)
static member AwaitIAsyncResult : iar:IAsyncResult * ?millisecondsTimeout:int -> Async<bool>
static member AwaitTask : task:Task -> Async<unit>
static member AwaitTask : task:Task<'T> -> Async<'T>
static member AwaitWaitHandle : waitHandle:WaitHandle * ?millisecondsTimeout:int -> Async<bool>
static member CancelDefaultToken : unit -> unit
static member Catch : computation:Async<'T> -> Async<Choice<'T,exn>>
static member FromBeginEnd : beginAction:(AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg:'Arg1 * beginAction:('Arg1 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg1:'Arg1 * arg2:'Arg2 * beginAction:('Arg1 * 'Arg2 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg1:'Arg1 * arg2:'Arg2 * arg3:'Arg3 * beginAction:('Arg1 * 'Arg2 * 'Arg3 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromContinuations : callback:(('T -> unit) * (exn -> unit) * (OperationCanceledException -> unit) -> unit) -> Async<'T>
static member Ignore : computation:Async<'T> -> Async<unit>
static member OnCancel : interruption:(unit -> unit) -> Async<IDisposable>
static member Parallel : computations:seq<Async<'T>> -> Async<'T []>
static member RunSynchronously : computation:Async<'T> * ?timeout:int * ?cancellationToken:CancellationToken -> 'T
static member Sleep : millisecondsDueTime:int -> Async<unit>
static member Start : computation:Async<unit> * ?cancellationToken:CancellationToken -> unit
static member StartAsTask : computation:Async<'T> * ?taskCreationOptions:TaskCreationOptions * ?cancellationToken:CancellationToken -> Task<'T>
static member StartChild : computation:Async<'T> * ?millisecondsTimeout:int -> Async<Async<'T>>
static member StartChildAsTask : computation:Async<'T> * ?taskCreationOptions:TaskCreationOptions -> Async<Task<'T>>
static member StartImmediate : computation:Async<unit> * ?cancellationToken:CancellationToken -> unit
static member StartWithContinuations : computation:Async<'T> * continuation:('T -> unit) * exceptionContinuation:(exn -> unit) * cancellationContinuation:(OperationCanceledException -> unit) * ?cancellationToken:CancellationToken -> unit
static member SwitchToContext : syncContext:SynchronizationContext -> Async<unit>
static member SwitchToNewThread : unit -> Async<unit>
static member SwitchToThreadPool : unit -> Async<unit>
static member TryCancelled : computation:Async<'T> * compensation:(OperationCanceledException -> unit) -> Async<'T>
static member CancellationToken : Async<CancellationToken>
static member DefaultCancellationToken : CancellationToken

Full name: Microsoft.FSharp.Control.Async

--------------------
type Async<'T>

Full name: Microsoft.FSharp.Control.Async<_>
static member Async.Sleep : millisecondsDueTime:int -> Async<unit>
static member Async.Parallel : computations:seq<Async<'T>> -> Async<'T []>
static member Async.RunSynchronously : computation:Async<'T> * ?timeout:int * ?cancellationToken:Threading.CancellationToken -> 'T
static member Async.StartWithContinuations : computation:Async<'T> * continuation:('T -> unit) * exceptionContinuation:(exn -> unit) * cancellationContinuation:(OperationCanceledException -> unit) * ?cancellationToken:Threading.CancellationToken -> unit
Multiple items
type PrimeFinder =
  new : unit -> PrimeFinder
  member IsPrime : n:int64 -> unit
  member PrimeFound : IEvent<PrimeFinder * (int64 * bool)>

Full name: fsharpeventsrxasync.PrimeFinder

--------------------
new : unit -> PrimeFinder
val primeFoundEvent : Event<PrimeFinder * (int64 * bool)>
Multiple items
module Event

from Microsoft.FSharp.Control

--------------------
type Event<'T> =
  new : unit -> Event<'T>
  member Trigger : arg:'T -> unit
  member Publish : IEvent<'T>

Full name: Microsoft.FSharp.Control.Event<_>

--------------------
type Event<'Delegate,'Args (requires delegate and 'Delegate :> Delegate)> =
  new : unit -> Event<'Delegate,'Args>
  member Trigger : sender:obj * args:'Args -> unit
  member Publish : IEvent<'Delegate,'Args>

Full name: Microsoft.FSharp.Control.Event<_,_>

--------------------
new : unit -> Event<'T>

--------------------
new : unit -> Event<'Delegate,'Args>
val x : PrimeFinder
member PrimeFinder.PrimeFound : IEvent<PrimeFinder * (int64 * bool)>

Full name: fsharpeventsrxasync.PrimeFinder.PrimeFound
property Event.Publish: IEvent<PrimeFinder * (int64 * bool)>
member PrimeFinder.IsPrime : n:int64 -> unit

Full name: fsharpeventsrxasync.PrimeFinder.IsPrime
val n : int64
member Event.Trigger : arg:'T -> unit
val primeFinder : PrimeFinder

Full name: fsharpeventsrxasync.primeFinder
property PrimeFinder.PrimeFound: IEvent<PrimeFinder * (int64 * bool)>
val e : int64 * bool
member PrimeFinder.IsPrime : n:int64 -> unit
val filter : predicate:('T -> bool) -> sourceEvent:IEvent<'Del,'T> -> IEvent<'T> (requires delegate and 'Del :> Delegate)

Full name: Microsoft.FSharp.Control.Event.filter
val snd : tuple:('T1 * 'T2) -> 'T2

Full name: Microsoft.FSharp.Core.Operators.snd
val add : callback:('T -> unit) -> sourceEvent:IEvent<'Del,'T> -> unit (requires delegate and 'Del :> Delegate)

Full name: Microsoft.FSharp.Control.Event.add
val findPrimesAsync : numbers:'a -> primeTester:('b -> unit) -> Async<unit>

Full name: fsharpeventsrxasync.findPrimesAsync
val numbers : 'a
val primeTester : ('a -> unit)
val findPrimes : ('a list -> Async<unit>)
val n : 'a list
val h : 'a
val t : 'a list
static member Async.SwitchToNewThread : unit -> Async<unit>
static member Async.SwitchToThreadPool : unit -> Async<unit>
type PrimeTime =
  {Time: DateTime;
   Prime: int64 option;
   ID: string;}
  override ToString : unit -> string

Full name: fsharpeventsrxasync.PrimeTime
PrimeTime.Time: DateTime
Multiple items
type DateTime =
  struct
    new : ticks:int64 -> DateTime + 10 overloads
    member Add : value:TimeSpan -> DateTime
    member AddDays : value:float -> DateTime
    member AddHours : value:float -> DateTime
    member AddMilliseconds : value:float -> DateTime
    member AddMinutes : value:float -> DateTime
    member AddMonths : months:int -> DateTime
    member AddSeconds : value:float -> DateTime
    member AddTicks : value:int64 -> DateTime
    member AddYears : value:int -> DateTime
    ...
  end

Full name: System.DateTime

--------------------
DateTime()
   (+0 other overloads)
DateTime(ticks: int64) : unit
   (+0 other overloads)
DateTime(ticks: int64, kind: DateTimeKind) : unit
   (+0 other overloads)
DateTime(year: int, month: int, day: int) : unit
   (+0 other overloads)
DateTime(year: int, month: int, day: int, calendar: Globalization.Calendar) : unit
   (+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int) : unit
   (+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, kind: DateTimeKind) : unit
   (+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, calendar: Globalization.Calendar) : unit
   (+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int) : unit
   (+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int, kind: DateTimeKind) : unit
   (+0 other overloads)
PrimeTime.Prime: int64 option
Multiple items
val int64 : value:'T -> int64 (requires member op_Explicit)

Full name: Microsoft.FSharp.Core.Operators.int64

--------------------
type int64 = Int64

Full name: Microsoft.FSharp.Core.int64

--------------------
type int64<'Measure> = int64

Full name: Microsoft.FSharp.Core.int64<_>
type 'T option = Option<'T>

Full name: Microsoft.FSharp.Core.option<_>
PrimeTime.ID: string
Multiple items
val string : value:'T -> string

Full name: Microsoft.FSharp.Core.Operators.string

--------------------
type string = String

Full name: Microsoft.FSharp.Core.string
val x : PrimeTime
override PrimeTime.ToString : unit -> string

Full name: fsharpeventsrxasync.PrimeTime.ToString
val sprintf : format:Printf.StringFormat<'T> -> 'T

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.sprintf
union case Option.Some: Value: 'T -> Option<'T>
val e : int64
union case Option.None: Option<'T>
DateTime.ToLongTimeString() : string
val pFound : IEvent<PrimeTime>

Full name: fsharpeventsrxasync.pFound
val map : mapping:('T -> 'U) -> sourceEvent:IEvent<'Del,'T> -> IEvent<'U> (requires delegate and 'Del :> Delegate)

Full name: Microsoft.FSharp.Control.Event.map
property DateTime.Now: DateTime
val fst : tuple:('T1 * 'T2) -> 'T1

Full name: Microsoft.FSharp.Core.Operators.fst
val checkStatusButton : obj

Full name: fsharpeventsrxasync.checkStatusButton
namespace System.Text
val csClicked : IEvent<PrimeTime>

Full name: fsharpeventsrxasync.csClicked
val reduced : IEvent<PrimeTime option * PrimeTime option>

Full name: fsharpeventsrxasync.reduced
val merge : event1:IEvent<'Del1,'T> -> event2:IEvent<'Del2,'T> -> IEvent<'T> (requires delegate and 'Del1 :> Delegate and delegate and 'Del2 :> Delegate)

Full name: Microsoft.FSharp.Control.Event.merge
val scan : collector:('U -> 'T -> 'U) -> state:'U -> sourceEvent:IEvent<'Del,'T> -> IEvent<'U> (requires delegate and 'Del :> Delegate)

Full name: Microsoft.FSharp.Control.Event.scan
val p : PrimeTime option
val c : PrimeTime option
val e : PrimeTime
val statusCheck : IEvent<PrimeTime>

Full name: fsharpeventsrxasync.statusCheck
property Option.IsSome: bool
val f : PrimeTime
property Option.Value: PrimeTime
namespace System.Windows
namespace System.Windows.Forms
val isPrime : x:int64 -> bool

Full name: fsharpeventsrxasync.isPrime
val x : int64
val check : (int64 -> bool)
val i : int64
Multiple items
val double : value:'T -> double (requires member op_Explicit)

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.double

--------------------
type double = Double

Full name: Microsoft.FSharp.Core.double
val sqrt : value:'T -> 'U (requires member Sqrt)

Full name: Microsoft.FSharp.Core.Operators.sqrt
val y : int64
val findPrimesAsync : numbers:'a list -> primeTester:('a -> unit) -> Async<unit>

Full name: fsharpeventsrxasync.findPrimesAsync
val numbers : 'a list
val runForm : unit -> unit

Full name: fsharpeventsrxasync.runForm
val panel : FlowLayoutPanel
Multiple items
type FlowLayoutPanel =
  inherit Panel
  new : unit -> FlowLayoutPanel
  member FlowDirection : FlowDirection with get, set
  member GetFlowBreak : control:Control -> bool
  member LayoutEngine : LayoutEngine
  member SetFlowBreak : control:Control * value:bool -> unit
  member WrapContents : bool with get, set

Full name: System.Windows.Forms.FlowLayoutPanel

--------------------
FlowLayoutPanel() : unit
property Control.Dock: DockStyle
type DockStyle =
  | None = 0
  | Top = 1
  | Bottom = 2
  | Left = 3
  | Right = 4
  | Fill = 5

Full name: System.Windows.Forms.DockStyle
field DockStyle.Fill = 5
property FlowLayoutPanel.WrapContents: bool
val form : Form
Multiple items
type Form =
  inherit ContainerControl
  new : unit -> Form
  member AcceptButton : IButtonControl with get, set
  member Activate : unit -> unit
  member ActiveMdiChild : Form
  member AddOwnedForm : ownedForm:Form -> unit
  member AllowTransparency : bool with get, set
  member AutoScale : bool with get, set
  member AutoScaleBaseSize : Size with get, set
  member AutoScroll : bool with get, set
  member AutoSize : bool with get, set
  ...
  nested type ControlCollection

Full name: System.Windows.Forms.Form

--------------------
Form() : unit
property Control.Controls: Control.ControlCollection
Control.ControlCollection.Add(value: Control) : unit
val startButton : Button
Multiple items
type Button =
  inherit ButtonBase
  new : unit -> Button
  member AutoSizeMode : AutoSizeMode with get, set
  member DialogResult : DialogResult with get, set
  member NotifyDefault : value:bool -> unit
  member PerformClick : unit -> unit
  member ToString : unit -> string
  event DoubleClick : EventHandler
  event MouseDoubleClick : MouseEventHandler

Full name: System.Windows.Forms.Button

--------------------
Button() : unit
val cancelButton : Button
val checkStatusButton : Button
val primeFinder : PrimeFinder
val csClicked : IEvent<PrimeTime>
event Control.Click: IEvent<EventHandler,EventArgs>
val pFound : IEvent<PrimeTime>
val reduced : IEvent<PrimeTime option * PrimeTime option>
val statusCheck : IEvent<PrimeTime>
override PrimeTime.ToString : unit -> string
val numbers : int64 list
val x : exn
static member Async.CancelDefaultToken : unit -> unit
Control.ControlCollection.AddRange(controls: Control []) : unit
type Application =
  static member AddMessageFilter : value:IMessageFilter -> unit
  static member AllowQuit : bool
  static member CommonAppDataPath : string
  static member CommonAppDataRegistry : RegistryKey
  static member CompanyName : string
  static member CurrentCulture : CultureInfo with get, set
  static member CurrentInputLanguage : InputLanguage with get, set
  static member DoEvents : unit -> unit
  static member EnableVisualStyles : unit -> unit
  static member ExecutablePath : string
  ...
  nested type MessageLoopCallback

Full name: System.Windows.Forms.Application
Application.Run() : unit
Application.Run(context: ApplicationContext) : unit
Application.Run(mainForm: Form) : unit