15

I have two streams signaling when some conditions change. I need an Observable which will fire true when all the conditions turn true. false when any of them turns false. If some of the conditions is false and another changes to false I don't need to raise events.

Here is how I'm doing it:

// Introducing current states
private bool cond1 = false;
private bool cond2 = false;

void MyHandlingMethod(IObservable<bool> c1, IObservable<bool> c2)
{
    c1.Subscribe(b => _state1 = b);
    c2.Subscribe(b => _state2 = b);

    var c3 = c1.Merge(c2).Select(_ => _cond1 && _cond2);

    c3.Subscribe(b => /* some action goes here /*);
    // ...
}

I want to know if it's the right way solve my problem and if there are any pitfals. Eg c3 subscription fires before c1 and c2 due to asynchronous nature of rx.

Pavel Murygin
  • 2,242
  • 2
  • 18
  • 26

3 Answers3

18

There is no need to keep state:

c3 = c1.CombineLatest(c2, (a, b) => a && b).DistinctUntilChanged()
Alex
  • 7,639
  • 3
  • 45
  • 58
  • 2
    Humbug - I've been looking over the API for ages and failed to find that! However, you probably want `DistinctUntilChanged` too. – Jon Skeet Mar 13 '13 at 14:36
  • [My go-to RX API reference](http://www.introtorx.com/content/v1.0.10621.0/12_CombiningSequences.html) - the CombineLatest example is this exact question (!) – Alex Mar 13 '13 at 14:40
  • I dont think this fully meets the requirements. Only if all values are false should the sequence produce a value of false, else stay silent. If all values are true then push a true value. This soln will push false if any value is false. – Lee Campbell Mar 22 '13 at 17:59
  • DistinctUntilChanged() should fit the bill then - I have edited the answer to make this more explicit (although I had mentioned it originally) – Alex Mar 23 '13 at 12:27
  • I dont think that is right still. c1.OnNext(false);c2.OnNext(true); That will combine false&&true->false. ie incorrectly produces false, regardless of the DistinctUntilChanged(); – Lee Campbell Mar 25 '13 at 19:08
  • You have a point - how can this be resolved without using nullable booleans? Perhaps `.StartWith(false)` on both initial streams (or the result before `.DistinctUntilChanged()`)? – Alex Mar 26 '13 at 09:17
  • Apologies, I read the question wrong. I thought he wanted values only when both were true or both were false. Alex you are right, StartWith(False) or a BehaviorSubject would fit the bill here. – Lee Campbell Mar 26 '13 at 10:02
2

Honestly, I'd probably go with the CombineLatest approach, but in the interests of touching the somewhat-untouched parts of the Rx framework...

Although it's not a perfect fit, you can use Observable.When/ Observable.And / Observable.Then pattern:

var firstStream = new Subject<bool>();
var secondStream = new Subject<bool>();
var thirdStream = new Subject<bool>();
var fourthStream = new Subject<bool>();

var query = Observable.When(firstStream
        .And(secondStream)
        .And(thirdStream)
        .And(fourthStream)
        .Then((v1,v2,v3,v4) => v1 & v2 & v3 & v4));

using(query.Subscribe(Console.WriteLine))
{
    firstStream.OnNext(true);
    secondStream.OnNext(true);
    thirdStream.OnNext(true);
    // output stream will fire after this statement
    fourthStream.OnNext(true);
    Console.ReadLine();
}

One benefit of this approach is you are only creating output events when all streams have data that combine according to the Then clause - it also reads rather nicely. That said, it has one critical failing for your use case: you must have data on each incoming stream to trigger the output stream:

using(query.Subscribe(Console.WriteLine))
{
    firstStream.OnNext(true);
    secondStream.OnNext(true);
    thirdStream.OnNext(true);
    // output stream will fire after this statement
    fourthStream.OnNext(true);

    // this WON'T raise false on the output!
    firstStream.OnNext(false);
    secondStream.OnNext(false);
    thirdStream.OnNext(false);
    // output stream will fire false after this statement
    fourthStream.OnNext(false);
    Console.ReadLine();
}
JerKimball
  • 16,584
  • 3
  • 43
  • 55
0

Like JerKimball, I was going to suggest using Joins (now When):

IObservable<bool> cond1 = new [] { false, true, true, false, false }.ToObservable();
IObservable<bool> cond2 = new [] { true, true, false, false, true }.ToObservable();
Func<bool, bool> isFalse = (x) => x == false;
Func<bool, bool> isTrue = (x) => x == true;
var trueC1s = cond1.Where(isTrue);
var falseC1s = cond1.Where(isFalse);
var trueC2s = cond2.Where(isTrue);
var falseC2s = cond2.Where(isFalse);
var trues = trueC1s.And(trueC2s).Then((_, __) => true);
var falses = trueC1s.And(falseC2s).Then((_, __) => false);
var falses2 = falseC1s.And(trueC2s).Then((_, __) => false);
var falses3 = falseC1s.And(falseC2s).Then((_, __) => false);
Observable.When(trues, falses, falses2, falses3).Dump();

It does get a bit messy when there is many Observables, however.

Richard Anthony Hein
  • 10,550
  • 3
  • 42
  • 62
  • Nice - true, it does get messy, but if you split it up into individual `Plan` statements like you have done here, you can actually "build up" the query via a looping construct. :) – JerKimball Mar 13 '13 at 15:56