2

The code snippet below is my attempt at creating the following functionality:

  • Create an observable sequence that subscribes to a collection of subjects
  • When one of the subjects in the collection produces a value, the sequence ends, invokes a method that returns a new set of subjects and restarts at 1.
  • When the subscription to the outer observable gets disposed the whole thing stops

Questions about my implementation:

  • Why does it work using subjectsSub.SelectMany(x => x).Merge() but not using subjectsSub.Merge()? (I would have expected the latter approach to work)
  • Is there a simpler, more elegant solution to this in the vast arsenal of Rx Functionality?

Update: This sample was actually back-ported from RxJS-Typescript to reach a broader audience with this question. The fact that the original runs in a single-threaded browser environment using Javascript should make it clearer why this "observable capturing" may work (it does and without dirty hacks like messing with RxJs internals).


    class Program
    {
      private static readonly Queue<IObservable<Unit>[]> observableDependencies = new Queue<IObservable<Unit>[]>();

      private static IObservable<Unit>[] EvaluateExpressionAndCaptureTouchedObservables(Func<object> expression)
      {
        // wire some traps for capturing any observables "touched" by expression
        expression();

        // return observables touched by expression (not in this example of course)
        if (observableDependencies.Count > 0)
          return observableDependencies.Dequeue();

        return new[] {Observable.Never<Unit>()}; // keep going
      }

      private static IObservable<Unit> CreateObservable(
Subject<IObservable<Unit>[]> capturedObservables, Stopwatch sw)
      {
        return Observable.Create<Unit>(observer =>
        {
          var isComplete = new Subject<Unit>();
          var isAborted = false;

          var disp = Scheduler.Default.Schedule(self =>
          {
            Console.WriteLine("** Next iteration at {0}", sw.Elapsed);

            capturedObservables.SelectMany(x => x).Merge().TakeUntil(isComplete).Subscribe(x =>
            {
              observer.OnNext(Unit.Default);

              // self-destruct
              isComplete.OnNext(Unit.Default);
            },
            () =>
            {
              Console.WriteLine("completed");

              if (!isAborted)
                self();
            });

            capturedObservables.OnNext(EvaluateExpressionAndCaptureTouchedObservables());
          });

          return new CompositeDisposable(Disposable.Create(() =>
          {
            isAborted = true;

            // self-destruct
            isComplete.OnNext(Unit.Default);
          }), disp);
        });
      }

      private static void Main(string[] args)
      {
        var sw = new Stopwatch();
        sw.Start();

        observableDependencies.Enqueue(new[]
        {
          Observable.Timer(TimeSpan.FromSeconds(10)).Select(x => Unit.Default)
        });

        observableDependencies.Enqueue(new[]
        {
          Observable.Timer(TimeSpan.FromSeconds(5)).Select(x => Unit.Default),
          Observable.Return(10).Select(x => Unit.Default)
        });

        observableDependencies.Enqueue(new[] {Observable.Timer(TimeSpan.FromSeconds(3)).Select(x => Unit.Default)});

        var capturedObservables = new Subject<IObservable<Unit>[]>();
        var obs = CreateObservable(capturedObservables, sw);

        var disp = obs.Subscribe(x => Console.WriteLine("** fired at {0}", sw.Elapsed));
        Console.ReadLine();

        disp.Dispose();
        Console.ReadLine();
      }
    }
Oliver Weichhold
  • 10,259
  • 5
  • 45
  • 87
  • Can you explain the actual problem you are trying to solve? I'd like to know more about where this "collection of subjects" is coming from - that part is assumed in your question, but it's the part that seems the most unidiomatic to me, particularly the idea of a downstream observer triggering their regeneration. I feel that just addressing the code above would be a disservice as I can't think of a scenario where I would do this. – James World Feb 24 '15 at 10:37
  • @JamesWorld I expect the use case is to provide a magic calculation method that does automatic capturing of observable dependencies and then subscribes to them so that the calculation can be re-run whenever a dependency changes. See `Knockout` computed properties for a JavaScript example. I've seen c# examples as well. A very useful feature that hides the complexity of Rx at a slight performance cost. – Brandon Feb 24 '15 at 15:33
  • OK I get what you've said... (I've used knockout/angular etc. bindings myself). The part I'm less clear on is why you need to throw away the subjects and get new ones instead of setting them up once and be done with it. Maybe that would be clear with an actual example... – James World Feb 24 '15 at 15:39
  • @JamesWorld - during re-evaluation you "forget" the previous dependencies and just capture the new set (which often turns out to be the exact same ones you had last evaluation). – Brandon Feb 24 '15 at 15:53
  • Example calculation: `result = a() ? b() : c();` would be a case where depedencies change during re-evaluation. – Brandon Feb 24 '15 at 15:55
  • @JamesWorld You've nailed it James. The purpose of this functionality is to capture references to Knockout-Style properties backed by an RxJS observable and automatically re-run an expression compiled by a modified version of the angular expression parser when any captured property observables change. – Oliver Weichhold Feb 24 '15 at 15:55
  • OK cool. Makes sense for RxJS, sure. – James World Feb 24 '15 at 16:40

2 Answers2

3

To answer your first question, SelectMany is needed because you have a 3-level deep observable: Subject of Array of Observable. Merge only flattens one level. SelectMany is just short hand for Select + Merge. So SelectMany.Merge is applying 2 flattening operators, which is what you need.

Second answer...it seems like you could just use Merge + FirstOrDefault + Defer + Repeat and not even use a subject:

var disp = Observable
    .Defer(() => EvaluateExpressionAndCaptureTouchedObservables()
        .Merge()
        .FirstOrDefault(Unit.Default))
    .Repeat()
    .Subscribe(...);

Defer runs the capture function each time it is subscribed

Merge to flatten the array of observables

FirstOrDefault ends the stream as soon as any of the observables produces a value. If all of them complete without producing a value, then it produces a single Unit.Default that you can observe.

Repeat resubscribes whenever it ends (due to FirstOrDefault), which triggers another capture (due to Defer).

This is obviously trivial to convert back to TypeScript...

Brandon
  • 38,310
  • 8
  • 82
  • 87
  • Is it possible that your example is missing the SelectMany operator? – Oliver Weichhold Feb 24 '15 at 16:15
  • @OliverWeichhold I'm sure it is possible. I didn't try to compile it. But based on your signature for `IObservable[]EvaluateExpressionAndCaptureTouchedObservables` it should not be required since there is an overload of `Merge` for `IEnumerable>` – Brandon Feb 24 '15 at 16:54
0

The more or less final version CreateObservable inspired by Brandon's suggestion. Which goes to show that 99% of the time you think you have to resort to scheduling stuff, you're doing it wrong™

private static IObservable<Unit> CreateObservable()
{
  return Observable.Create<Unit>(observer =>
  {
    var innerDisp = Observable.Defer(() =>
    {
      return Observable.Merge(
        EvaluateExpressionAndCaptureTouchedObservables(() => false))
      .Take(1);  // done when any observable produces a value
    })
    .Repeat()
    .Subscribe(x =>
    {
      observer.OnNext(Unit.Default);
    });

    return innerDisp;
  });
}
Oliver Weichhold
  • 10,259
  • 5
  • 45
  • 87
  • Are you using the `Buffer().Where()` just to throttle evaluations? If so, you might try `Throttle` (called `debounce` in RxJs) or `Sample` instead. – Brandon Feb 25 '15 at 16:57