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/

zerkms
  • 249,484
  • 69
  • 436
  • 539
  • 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 http://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. – user1009908 Dec 22 '14 at 22:29
  • @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? – zerkms Dec 23 '14 at 01:12

4 Answers4

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.

m1el
  • 176
  • 5
  • "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... ) – zerkms Dec 22 '14 at 10:15
  • 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. – zerkms Dec 22 '14 at 10:22
  • 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. – m1el Dec 22 '14 at 11:44
  • I don't understand why Subjects are needed here. Also, the conversion fns are reversed. ;) Wouldn't this do? http://jsfiddle.net/kcv15h6p/1/ – user1009908 Dec 22 '14 at 21:54
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);
Brandon
  • 38,310
  • 8
  • 82
  • 87
  • 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 recursion – zerkms Dec 22 '14 at 19:39
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/

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.

Community
  • 1
  • 1
user1009908
  • 1,390
  • 13
  • 22