2

In learning about Rx I've come across an often repeated rule about Observables that is spelled out in The Observable Contract.

Upon issuing an OnCompleted or OnError notification, it may not thereafter issue any further notifications.

This makes good sense to me, since it would be confusing to have an Observable continue to produce values after it has completed, but when I tested the Observable.Range method in .NET I noticed that it does not exhibit that behavior, and in fact many Observables violate this rule.

var rangeObservable = Observable.Range(0, 5);

rangeObservable.Subscribe(Console.WriteLine, () => Console.WriteLine("Done first!"));
Console.ReadLine();

rangeObservable.Subscribe(Console.WriteLine, () => Console.WriteLine("Done second!"));
Console.ReadLine();

//Output:
//0
//1
//2
//3
//4
//Done first!

//0
//1
//2
//3
//4
//Done second!

Clearly rangeObservable has called OnComplete two times and produced values after the first OnComplete. This leads me to believe that this is not a rule about Observables but instead a rule about Subscriptions. That is, an Observable may produce as many terminating messages as it wants, and even produce values after it has, so long as each Subscription only receives one terminating message and receives no further messages after that.

Do they actually mean Subscription when it says Observable? Are they truly different things? Do I have a fundamental misunderstanding of the model?

gitbox
  • 803
  • 7
  • 14

2 Answers2

4

The observable contract must be valid for any Observable being observed. Whether or not anything happens while the Observable is unobserved is left to the implementation of the observable.

It helps to think of analog in Enumerable - Observable being the dual of Enumerable. In enumerables, you would have range = Enumerable.Range(0, 5), and you would use the range similar to the above:

range.ForEach(Console.WriteLine); //prints 0 - 4

range.ForEach(Console.WriteLine); //prints 0 - 4 again

and find that this is perfectly acceptable behavior, because the the actual number generator gets created only when GetEnumerator is called. Similarly, in Observable, the equivalent method is Subscribe.

The implementation of range is something like:

        static IObservable<int> Range(int start, int count)
        {
            return Observable.Create<int>(observer =>
            {
                for (int i = 0; i < count; i++)
                    observer.OnNext(start + i);

                observer.OnCompleted();

                return Disposable.Empty;
            });
        }

Here, the observer => {...} function is called every time there is a subscription. The work gets done in the subscribe method. You can easily see that it (1) pushes the same sequence for every observer, (2) it only completes once per observer.

These observables where something happens only when you observe them are called cold observables. Here's an article describing the concept.

Note

The Range is a very naive implementation, just for illustration purposes. The method won't return the disposable until it completes - so Disposable.Empty is acceptable. A proper implementation would run the work on a scheduler and use a checked disposable to see if the subscription has been disposed before continuing the loop.

The takeaway is that implementing the observable contract by hand is hard, and this is why the Rx library exists - to build functionality by composition.

Asti
  • 12,447
  • 29
  • 38
  • Perfect. The shift in thought that allowed me to make some sense of this was to realize that an Observable shouldn’t be considered as an “object” but instead as a “subscribe method”. Just a method to call every time something wants to subscribe. That and the fact that I don’t really know how would implement the Range operator without this model. What are you going to do, just make every range operator start as soon as it’s constructed? No one would be able to get any values off of it! – gitbox Jan 03 '17 at 00:14
  • @gitbox Haha. Right on the money. – Asti Jan 03 '17 at 15:54
  • @Asti - I like your answer for everything except your implementation of `Range` - if ever you find yourself writing `return Disposable.Empty;` in a `.Create` method you **are** doing something wrong. – Enigmativity Jan 04 '17 at 07:15
  • @Enigmativity You're completely right. It's a very naive implementation, just to illustrate. The method won't return the disposable until it completes though - so I reasoned that it doesn't matter what it returns. A proper implementation would run the work on a scheduler and possibly use a checked disposable, but it might have made the explanation more confusing. – Asti Jan 04 '17 at 11:54
  • @Asti - It would be nice to add that explanation to the answer. I would hate someone to use the implementation in production code. – Enigmativity Jan 04 '17 at 11:57
  • @Asti - That's a very good explanation. I especially like your takeaway at the end. – Enigmativity Jan 05 '17 at 00:00
4

Observable.Range returns a cold observable, which means it "replays" it's behavior for each subscriber. Since the "OnNext* OnComplete|OnError" contract only applies to a subscription, this is totally fine.

For more information on hot/cold observables, see my answer on "IConnectableObservables in Rx"

Community
  • 1
  • 1
Richard Szalay
  • 83,269
  • 19
  • 178
  • 237
  • The more I think about it the more it seems this answer is quite obvious! When we talk about some Observable producing values it refers to those of a single subscription. The case I wasn’t thinking of was when an Observable has multiple observers, it would therefore send numerous OnCompleted messages albeit at the “same time”. Since that is a common scenario the Observable contract would never disallow it. Doh! – gitbox Jan 03 '17 at 00:08
  • 1
    I’m familiar with the concept of cold and hot observables. My question could have been phrased “Do all cold observables break The Observable Contract?” since all of them would with my previous understanding. – gitbox Jan 03 '17 at 00:14