Why does Observable.range freeze when run on the GUI thread?

Viewed 156
module StackTenButtons.Try2

open System
open System.Windows
open System.Windows.Controls

open System.Reactive.Linq
open System.Reactive.Disposables
open FSharp.Control.Reactive

let control c l = 
    Observable.Create (fun (sub : IObserver<_>) ->
        let c = c()
        let d = new CompositeDisposable()
        List.iter (fun x -> d.Add(x c)) l
        sub.OnNext(c)
        d :> IDisposable
        )
let do' f c = f c; Disposable.Empty
let prop s v c = Observable.subscribe (s c) v

let w =
    control Window [
        prop (fun t v -> t.Content <- v) <| control StackPanel [
            do' (fun pan ->
                Observable.range 0 10
                |> Observable.subscribe (fun x -> pan.Children.Add(Button(Content=sprintf "Button %i" x)) |> ignore)
                |> ignore
                )
            ]
        ]

[<STAThread>]
[<EntryPoint>]
let main _ = w.Subscribe (Application().Run >> ignore); 0

I am trying to make a small proof of concept library for reactive UIs and I've encountered this problem when trying to write a function that adds more than a single control to the parent. Standard property setting works when they are singletons, but not when using functions like Observable.range which are iterators.

Is it possible to make this work?

As F# needs some stuff to be added manually to the project file so WPF can be used, here is the repo for this.

1 Answers

The default scheduler for Range is Scheduler.CurrentThread.

CurrentThread and Immediate have behavior that sometimes results in a perpetual trampoline or a deadlock particularly when attempting to be used synchronously with an Observable.Create or similar un-scheduled cold observables.

The exact reasons why they lock up are difficult to describe, but are similar to the behavior found here and here.

 Observable.Create (fun (sub : IObserver<_>) -> 
        sub.OnNext(1)
        sub.OnNext(2)
        sub.OnNext(3)
        d :> IDisposable //<-- this dispose should cancel all `OnNext`
 )

The above dispose can never be called, to prevent items from being emitted - until after the items have been emitted. If you manually construct an observable, try to make it take in a scheduler argument.

DispatcherScheduler being back in Rx.NET core might still be some ways off. Here's an implementation of a minimal DispatcherScheduler:

type DispatcherScheduler =
    static member Instance = {
     new IScheduler with
         member _.Now = DateTimeOffset.Now
         member _.Schedule<'S>(state: 'S, action: Func<IScheduler, 'S, IDisposable>) : IDisposable = 
            let op = Application.Current.Dispatcher.InvokeAsync(fun () -> action.Invoke(DispatcherScheduler.Instance, state))
            Disposable.Create(fun () -> op.Abort() |> ignore)
         member _.Schedule<'S>(state: 'S, dueTime: TimeSpan, action: Func<IScheduler, 'S, IDisposable>) : IDisposable = failwith "Not Impl"
         member _.Schedule<'S>(state: 'S, dueTime: DateTimeOffset, action: Func<IScheduler, 'S, IDisposable>) : IDisposable = failwith "Not Impl"
    }
Related