How to only emit consistent calculations?

时间:2016-07-11 22:42:41

标签: system.reactive reactive-programming

I'm using reactive programming to do a bunch of calculations. Here is a simple example that tracks two numbers and their sum:

static void Main(string[] args) {
    BehaviorSubject<int> x = new BehaviorSubject<int>(1);
    BehaviorSubject<int> y = new BehaviorSubject<int>(2);
    var sum = Observable.CombineLatest(x, y, (num1, num2) => num1 + num2);
    Observable
        .CombineLatest(x, y, sum, (xx, yy, sumsum) => new { X = xx, Y = yy, Sum = sumsum })
        .Subscribe(i => Console.WriteLine($"X:{i.X} Y:{i.Y} Sum:{i.Sum}"));
    x.OnNext(3);
    Console.ReadLine();
}

This generates the following output:

X:1 Y:2 Sum:3
X:3 Y:2 Sum:3 
X:3 Y:2 Sum:5

Notice how second output result is "incorrect" because it is showing that 3+2=3. I understand why this is happening (x is updated before the sum is updated) but I want my output calculations to be atomic/consistent - no value should be emitted until all dependent calculations are complete. My first approach was this...

Observable.When(sum.And(Observable.CombineLatest(x, y)).Then((s, xy) => new { Sum = s, X = xy[0], Y = xy[1] } ));

This seems to work for my simple example. But my actual code has LOTS of calculated values and I couldn't figure out how to scale it. For example, if there was a sum and squaredSum, I don't know how to wait for each of these to emit something before taking action.

One method that should work (in-theory) is to timestamp all the values I care about, as shown below.

Observable
    .CombineLatest(x.Timestamp(), y.Timestamp(), sum.Timestamp(), (xx, yy, sumsum) => new { X = xx, Y = yy, Sum = sumsum })
    .Where(i=>i.Sum.Timestamp>i.X.Timestamp && i.Sum.Timestamp>i.Y.Timestamp)
    // do the calculation and subscribe

This method could work for very complicated models. All I have to do is ensure that no calculated value is emitted that is older than any core data value. I find this to be a bit of a kludge. It didn't actually work in my console app. When I replaced Timestamp with a custom extension that assigned a sequential int64 it did work.

What is a simple, clean way to handle this kind of thing in general?

=======

I'm making some progress here. This waits for a sum and sumSquared to emit a value before grabbing the data values that triggered the calculation.

var all = Observable.When(sum.And(sumSquared).And(Observable.CombineLatest(x, y)).Then((s, q, data) 
    => new { Sum = s, SumSquared = q, X = data[0], Y = data[1] }));

3 个答案:

答案 0 :(得分:1)

这应该做你想要的:

Observable.CombineLatest(x, y, sum)
    .DistinctUntilChanged(list => list[2])
    .Subscribe(list => Console.WriteLine("{0}+{1}={2}", list[0], list[1], list[2]));

等待总和更新,这意味着它的所有来源也必须已更新。

答案 1 :(得分:0)

问题不在于 x在更新总和之前更新本身。这真的与你构建查询的方式有关。

您已经有效地创建了两个查询:Observable.CombineLatest(x, y, (num1, num2) => num1 + num2)&amp; Observable.CombineLatest(x, y, sum, (xx, yy, sumsum) => new { X = xx, Y = yy, Sum = sumsum })。因为在每个订阅x时,您都会创建两个订阅。这意味着当x更新时,会发生两次更新。

您需要避免创建两个订阅。

如果您编写如下代码:

BehaviorSubject<int> x = new BehaviorSubject<int>(1);
BehaviorSubject<int> y = new BehaviorSubject<int>(2);

Observable
    .CombineLatest(x, y, (num1, num2) => new
    {
        X = num1,
        Y = num2,
        Sum = num1 + num2
    })
    .Subscribe(i => Console.WriteLine($"X:{i.X} Y:{i.Y} Sum:{i.Sum}"));

x.OnNext(3);

...然后你正确地得到了这个输出:

X:1 Y:2 Sum:3
X:3 Y:2 Sum:5

答案 2 :(得分:0)

我已经开始更多地了解这一点。以下是我尝试完成的更详细的示例。这是验证名字和姓氏的一些代码,并且只应在两个部分都有效时生成整个名称。正如你所看到的,我试图使用一堆小的独立定义函数,比如&#34; firstIsValid&#34;,然后将它们组合起来计算更复杂的东西。

我面临的挑战似乎是尝试关联我的功能中的输入和输出。例如,&#34; firstIsValid&#34;生成一个输出,说明某些名字有效,但不告诉你哪一个。在下面的选项2中,我可以使用Zip来关联它们。

如果验证函数不为每个输入生成一个输出,则此策略不起作用。例如,如果用户正在键入网址并且我们正在尝试在网络上验证它们,那么我们可能会做一个节流和/或切换。单个&#34; webAddressIsValid&#34;可能有10个网址。在那种情况下,我认为我必须在输入中包含输出。也许有一个IObservable&gt;其中字符串是网址,而bool是否有效。

static void Main(string[] args) {
    var first = new BehaviorSubject<string>(null);
    var last = new BehaviorSubject<string>(null);
    var firstIsValid = first.Select(i => string.IsNullOrEmpty(i) || i.Length < 3 ? false : true);
    var lastIsValid = last.Select(i => string.IsNullOrEmpty(i) || i.Length < 3 ? false : true);

    // OPTION 1 : Does not work
    // Output: bob smith, bob, bob roberts, roberts
    // firstIsValid and lastIsValid are not in sync with first and last
    //var whole = Observable
    //    .CombineLatest(first, firstIsValid, last, lastIsValid, (f, fv, l, lv) => new {
    //        First = f,
    //        Last = l,
    //        FirstIsValid = fv,
    //        LastIsValid = lv
    //    })
    //    .Where(i => i.FirstIsValid && i.LastIsValid)
    //    .Select(i => $"{i.First} {i.Last}");

    // OPTION 2 : Works as long as every change in a core data value generates one calculated value
    // Output: bob smith, bob robert
    var firstValidity = Observable.Zip(first, firstIsValid, (f, fv) => new { Name = f, IsValid = fv });
    var lastValidity = Observable.Zip(last, lastIsValid, (l, lv) => new { Name = l, IsValid = lv });
    var whole =
        Observable.CombineLatest(firstValidity, lastValidity, (f, l) => new { First = f, Last = l })
        .Where(i => i.First.IsValid && i.Last.IsValid)
        .Select(i => $"{i.First.Name} {i.Last.Name}");

    whole.Subscribe(i => Console.WriteLine(i));

    first.OnNext("bob");
    last.OnNext("smith");
    last.OnNext(null);
    last.OnNext("roberts");
    first.OnNext(null);

    Console.ReadLine();
}

这里的另一种方法。每个值都获得一个版本号(如时间戳)。只要计算出的值比数据(或它依赖的其他计算值)旧,我们就可以忽略它。

public class VersionedValue {
    static long _version;
    public VersionedValue() { Version = Interlocked.Increment(ref _version); }
    public long Version { get; }
}

public class VersionedValue<T> : VersionedValue {
    public VersionedValue(T value) { Value = value; }
    public T Value { get; }
    public override string ToString() => $"{Value} {Version}";
}

public static class ExtensionMethods {
    public static IObservable<VersionedValue<T>> Versioned<T>(this IObservable<T> values) => values.Select(i => new VersionedValue<T>(i));
    public static VersionedValue<T> AsVersionedValue<T>(this T obj) => new VersionedValue<T>(obj);
}

static void Main(string[] args) {
    // same as before
    //
    var whole = Observable
        .CombineLatest(first.Versioned(), firstIsValid.Versioned(), last.Versioned(), lastIsValid.Versioned(), (f, fv, l, lv) => new {
            First = f,
            Last = l,
            FirstIsValid = fv,
            LastIsValid = lv
        })
        .Where(i => i.FirstIsValid.Version > i.First.Version && i.LastIsValid.Version > i.Last.Version)
        .Where(i => i.FirstIsValid.Value && i.LastIsValid.Value)
        .Select(i => $"{i.First.Value} {i.Last.Value}");