Safe update for 2 dependent streams
Asked Answered
D

4

1

As an exercise I'm trying to build 2 dependent streams which update one another.

The test application is simply an "Inches <-> Centimeters" converter, with both inputs editable.

The issue I am experiencing is that I cannot get how can I stop recursion that causes one field change.

To better explain the issue let's have a look at the relevant part of code:

var cmValue = new Rx.BehaviorSubject(0),
    inValue = new Rx.BehaviorSubject(0);

# handler #1
cmValue.distinctUntilChanged().subscribe(function(v) {
    inValue.onNext(cmToIn(v));
});

# handler #2
inValue.distinctUntilChanged().subscribe(function (v) {
    cmValue.onNext(inToCm(v));
});

So we define to Subjects each of which holds the current corresponding value.

Now imagine we change the value in inches to 2 (using inValue.onNext(2); or via keyboard).

What happens next - is the handler #2 is triggered and it invokes a corresponding recalculation of a value in centimeters. Which results to cmValue.onNext(0.7874015748031495).

This call in fact is then handled by handler #1 and causes the value in inches (the one we put manually) to be recalculated, using 0.7874015748031495 * 2.54 formula which causes another inValue.onNext(1.99999999999999973) call.

Luckily - due to FP rounding error that's where we stop. But in other scenarios this may lead to more loops or even to an infinite recursion.

As you can see - I partially solved the issue applying .distinctUntilChanged() which at least protects us from an infinite recursion on any change, but as we can see - in this case it's does not solve the problem entirely since values are not identical (due to FP operations nature).

So the question is: how would one implement a generic two-way binding that does not cause self-recursion at all?

I emphasized generic to make a note that using .select() with rounding would be a partial solution for this particular issue, and not the generic one (which I and everyone else would prefer).

The complete code and demo: http://jsfiddle.net/ewr67eLr/

Disavowal answered 22/12, 2014 at 4:43 Comment(2)
It's unclear from the question why you need recursion. You can have events from input A update B, and events from input B update A without streams referring to each other. I.e. assigning to cmElement.value doesn't generate an event, so there's no loop. For example jsfiddle.net/kcv15h6p/1 You've created a loop here by introducing connected Subjects that you don't need to solve the stated problem. You can, however, safely create loops in Rx when you need them. Perhaps update the question with a problem that requires a loop.Stepdame
@user1009908: I do not need recursion. My question is how to eliminate it. "Perhaps update the question with a problem that requires a loop" --- the problem is already described, sorry. And I like your solution. Keen to put it as an answer?Disavowal
S
1

In your demo you have two input fields. "Keyup" events for this inputs will be the information source, and inputs value will be destination. In this case you don't need mutable states for checking updates of observables.

Rx.Observable.fromEvent(cmElement, 'keyup')
    .map(targetValue)
    .distinctUntilChanged()
    .map(cmToIn)
    .startWith(0)
    .subscribe(function(v){ inElement.value = v; });

Rx.Observable.fromEvent(inElement, 'keyup')
    .map(targetValue)
    .distinctUntilChanged()
    .map(inToCm)
    .startWith(0)
    .subscribe(function(v){ cmElement.value = v; });

Check my example here: http://jsfiddle.net/537Lrcot/2/

Substantialism answered 23/12, 2014 at 7:47 Comment(0)
O
2

I had a similar task to solve on my project.

First, you have to select your ground truth - the value that is going to represent your measurement, let's say you choose centimeters.

Now you're working with only one stream that is getting updates from several sources.

Since have to store a value that cannot be represented precisely as you input it, you have to output it with fewer significant digits than the whole float. A person is not likely to measure inches to the precision of 11 significant digits, so there is no point to display converted value to that precision.

function myRound(x, digits) {
    var exp = Math.pow(10, digits);
    return Math.round(x * exp) / exp;
}

cmValue.subscribe(function(v) {
    if (document.activeElement !== cmElement) {
        cmElement.value = myRound(v, 3).toFixed(3);
    }
    if (document.activeElement !== inElement) {
        inElement.value = myRound(cmToIn(v), 3).toFixed(3);
    }
});

So far, here's a working example: http://jsfiddle.net/ewr67eLr/4/

What's left is an edge case when we changed our focus to auto-computed value and then that value recalculates our first value with different digits.

This can be solved by creating streams for values that have been changed by user:

    cmInputValue = new Rx.BehaviorSubject(0),
    inInputValue = new Rx.BehaviorSubject(0),
        ...
Rx.Observable.fromEvent(cmElement, 'input').subscribe(function (e) {
    cmInputValue.onNext(e.target.value);
});
Rx.Observable.fromEvent(inElement, 'input').subscribe(function (e) {
    inInputValue.onNext(e.target.value);
});
cmInputValue.distinctUntilChanged().subscribe(function (v) {
    cmValue.onNext(v);
});
inInputValue.distinctUntilChanged().subscribe(function (v) {
    cmValue.onNext(inToCm(v));
});

http://jsfiddle.net/ewr67eLr/6/

Now this is the best way I could solve this task.

Omnibus answered 22/12, 2014 at 10:9 Comment(4)
"First, you have to select your ground truth" --- to be precise it was like that in the initial implementation, then I deliberately changed it to not have one so that the answer was unbiased :-) Reading the answer further... )Disavowal
Have thoroughly read it. Yep, it mostly matches to what I suspected it will be. And honestly document.activeElement trick looks dirty. I mean, I expected RxJS would allow to design the such kind of app in a nicer way.Disavowal
As far as I can understand, what you're trying to do has no "nice" mathematical representation, so it's either going to be ugly or complicated.Omnibus
I don't understand why Subjects are needed here. Also, the conversion fns are reversed. ;) Wouldn't this do? jsfiddle.net/kcv15h6p/1Stepdame
D
1

Here's a purely algorithmic way to solve the problem (ie it doesn't rely upon the specific state of the DOM). Basically just use a variable to detect recursion and abort the update.

/* two way binding */
var twowayBind = function (a, b, aToB, bToA) {
    var updatingA = 0,
        updatingB = 0,
        subscribeA = new Rx.SingleAssignmentDisposable(),
        subscribeB = new Rx.SingleAssignmentDisposable(),
        subscriptions = new Rx.CompositeDisposable(subscribeA, subscribeB);

    subscribeA.setDisposable(a.subscribe(function (value) {
        if (!updatingB) {
            ++updatingA;
            b.onNext(aToB(value));
            --updatingA;
        }
    }));

    subscribeB.setDisposable(b.subscribe(function (value) {
        if (!updatingA) {
            ++updatingB;
            a.onNext(bToA(value));
            --updatingB;
        }
    });

    return subscriptions;
};

var cmValue = new BehavoirSubject(0),
    inValue = new BehaviorSubject(0),
    binding = twowayBind(cmValue, inValue, cmToIn, inToCm);
Deliladelilah answered 22/12, 2014 at 15:4 Comment(1)
Yep, my alternative original solution was similar: I wanted to create a shared stream that would carry an object with both value and a flag if you need to propagate recursionDisavowal
S
1

In your demo you have two input fields. "Keyup" events for this inputs will be the information source, and inputs value will be destination. In this case you don't need mutable states for checking updates of observables.

Rx.Observable.fromEvent(cmElement, 'keyup')
    .map(targetValue)
    .distinctUntilChanged()
    .map(cmToIn)
    .startWith(0)
    .subscribe(function(v){ inElement.value = v; });

Rx.Observable.fromEvent(inElement, 'keyup')
    .map(targetValue)
    .distinctUntilChanged()
    .map(inToCm)
    .startWith(0)
    .subscribe(function(v){ cmElement.value = v; });

Check my example here: http://jsfiddle.net/537Lrcot/2/

Substantialism answered 23/12, 2014 at 7:47 Comment(0)
S
1

As noted in my comment, this problem doesn't require a loop. It also doesn't require Subjects, or document.activeElement. You can have events from input A update B, and events from input B update A without streams referring to each other.

Example here:

http://jsfiddle.net/kcv15h6p/1/

relevant bits here:

var cmElement = document.getElementById('cm'),
    inElement = document.getElementById('in'),
    cmInputValue = Rx.Observable.fromEvent(cmElement, 'input').map(evToValue).startWith(0),
    inInputValue = Rx.Observable.fromEvent(inElement, 'input').map(evToValue).startWith(0);


inInputValue.map(inToCm).subscribe(function (v) {
    cmElement.value = myRound(v, 3).toFixed(3);
});

cmInputValue.map(cmToIn).subscribe(function (v) {
    inElement.value = myRound(v, 3).toFixed(3);
});

For problems that really require loops, you can create loops with defer, as pointed out in Brandon's answer to this question:

Catch circular dependency between observables

Like any looping construct, you have to handle the exit condition to avoid an infinite loop. You can do this with operators like take(), or distinctUntilChanged(). Note that the latter takes a comparator, so you can, for example, use object identity (x, y) => x === y, to exit a loop.

Stepdame answered 24/12, 2014 at 20:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.