Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Do F# observable events obviate, mediate, or are not related to the need for weak references?

Since observables are typically IDisposable how does that change, if at all, the need to use weak references in event handlers, or any other event based memory leak/GC locked referencing?

While my primary concern/need is for WPF I'm looking for the broader example and trying to understand where I may need weak references.

F#'s Observable.add doesn't provide a way to unhook the event, so I'm thinking it's less likely to be a source of leaks. Sample code:

type Notifier() = 
    let propChanged = new Event<_,_>()
    member __.Foo() = ()
    interface INotifyPropertyChanged with
        [<CLIEvent>]
        member __.PropertyChanged = propChanged.Publish
    abstract member RaisePropertyChanged : string -> unit
    default x.RaisePropertyChanged(propertyName : string) = propChanged.Trigger(x, PropertyChangedEventArgs(propertyName))


Notifier() :?> INotifyPropertyChanged
|> Observable.add(fun _ -> printfn "I'm hooked on you")
like image 975
Maslow Avatar asked Feb 23 '17 13:02

Maslow


People also ask

What does f say about f?

Given a function f, the derivative f' can be used to get important information about f. For instance, f is increasing when f'>0. The second derivative gives useful concavity information.

What does f mean in math?

more ... A special relationship where each input has a single output. It is often written as "f(x)" where x is the input value.

What do we call f?

1. the sixth letter and fourth consonant of the modern English alphabet. 2. a speech sound represented by this letter, usually a voiceless labio-dental fricative, as in fat.


1 Answers

F#'s Observable.add doesn't provide a way to unhook the event, so I'm thinking it's less likely to be a source of leaks

It's actually the opposite. Observable.add, by the docs, permanently subscribes to the event, and forces a "leak". It's effectively doing an event handler addition that has no way to unsubscribe.

In general, with Observable (in F# and C#), you should favor using .subscribe, and disposing of the subscription handle when you're done.

As @rmunn mentioned, Gjallarhorn can serve as an alternative to using observables in some scenarios (and integrates nicely with them as needed). While writing it, one of my main goals was to make it so that subscriptions don't leak - all of the subscriptions use a hybrid push/pull model based on weak references, which prevents many of the problems with leaking in event and observable based code.

To demonstrate, I've thrown together a variation on your code, using both observables and Gjallarhorn's signals. If you run this in a release build, outside of the debugger, you'll see the difference:

type Notifier() = 
    let propChanged = new Event<_,_>()
    member __.Foo() = ()
    interface INotifyPropertyChanged with
        [<CLIEvent>]
        member __.PropertyChanged = propChanged.Publish
    abstract member RaisePropertyChanged : string -> unit
    default x.RaisePropertyChanged(propertyName : string) = propChanged.Trigger(x, PropertyChangedEventArgs(propertyName))

let obs () =
    use mre = new ManualResetEvent(false)

    let not = Notifier()

    do       
       let inpc = not :> INotifyPropertyChanged
       inpc.PropertyChanged 
       |> Observable.add (fun p -> printfn "Hit %s!" p.PropertyName)       

       async {
            for i in [0 .. 10] do
                do! Async.Sleep 100
                printfn "Raising"
                not.RaisePropertyChanged <| sprintf "%d" i
            mre.Set () |> ignore
       } |> Async.Start

       printfn "Exiting block"

    GC.Collect() // Force a collection, to "cleanup"
    mre.WaitOne() |> ignore

let signals () =
    use mre = new ManualResetEvent(false)

    let not = Mutable.create 0

    do
       not 
       |> Signal.Subscription.create (fun v -> printfn "Hit %d!" v)
       |> ignore // throw away subscription handle

       async {
            for i in [0 .. 10] do
                do! Async.Sleep 100 
                printfn "Setting"
                not.Value <- i                
            mre.Set () |> ignore
       } |> Async.Start

       printfn "Exiting block"

    GC.Collect() // Force a collection, to "cleanup"
    mre.WaitOne() |> ignore


[<STAThread>]
[<EntryPoint>]
let main _ =
    printfn "Using observable"
    obs ()

    printfn "Using signals"
    signals ()

    1

Note that both do something similar - they create a "source", then, in a separate scope, subscribe to it and throw away the disposable subscription handle (Observable.add is nothing but subscribe |> ignore - see code for details.). When running in a release build outside of the debugger (the debugger prevents cleanup from happening), you see:

Using observable
Exiting block
Raising
Hit 0!
Raising
Hit 1!
Raising
Hit 2!
Raising
Hit 3!
Raising
Hit 4!
Raising
Hit 5!
Raising
Hit 6!
Raising
Hit 7!
Raising
Hit 8!
Raising
Hit 9!
Raising
Hit 10!
Using signals
Exiting block
Setting
Setting
Setting
Setting
Setting
Setting
Setting
Setting
Setting
Setting
Setting
Press any key to continue . . .

In the observable case, the call to .add permanently holds a reference to the notifier, preventing it from being garbage collected. With signals, the signal subscription will GC, and "unhook" automatically, preventing the calls from Hit from ever being displayed.

like image 155
Reed Copsey Avatar answered Sep 20 '22 03:09

Reed Copsey