DEV Community

Cover image for Cyclic Reactive Dependencies
Jin
Jin

Posted on • Originally published at mol.hyoo.ru

Cyclic Reactive Dependencies

Sometimes we can end up with cyclical dependencies. Sometimes we may want to do them intentionally. For example, when implementing a converter between degrees Celsius and Fahrenheit, where the user can change any of the two values, and the second should be recalculated automatically.

However, in the vast majority of cases, cyclic dependencies indicate a problem with logic, so they are usually avoided. Fortunately, the logic of even a degree converter can always be rewritten so that there are no cyclic dependencies.

So let's look at how different systems react to this emergency situation.

🚫 Unreal: Impossible
πŸ’€ Infinite: Endless loop
🎰 Limbo: Arbitrary result
πŸŒ‹ Fail: Causes an error

🚫 Unreal

It's quite tempting to make it syntactically impossible to create loops. For example, we can require, when creating a state, that all of its dependencies already exist. This is typically the case with push libraries.

It doesn't sound bad. However, we threw out the baby with the bathwater. That is, we have extremely limited ourselves in what kind of logic of invariants we are able to describe. In particular, this practically puts an end to the dynamic configuration of data streams. For example, it will no longer be possible to implement a spreadsheet on such an architecture.

πŸ’€ Infinite

A number of libraries simply go into an endless loop, constantly updating the same states.

For Angular and React, for example, this is typical behavior. There is even a crutch - a limit on the number of recalculations of one invariant. But we'll talk about this later.

🎰 Limbo

There is also a very strange solution - when indirectly accessing the state that is currently being calculated, its previous value is used.

Depending on the order of calculations, this approach gives different results. That is, not only does the state turn out to be inconsistent, but also the behavior of the application becomes not stable, but begins to depend on the weather on Mars.

πŸŒ‹ Fail

The best solution is to detect the loop at runtime and throw an exception.

Further processing proceeds in the same way as with any other emergency situations. So it is especially important here that the system handles exceptions correctly.

Cycles Cutting Practice

As the application runs, dependencies are constantly rearranged to the point of being in diametrically opposite directions. For example, let's take a temperature converter, where, depending on what temperature we set explicitly, the second temperature should be calculated as a derived state. A naive implementation might look something like this:

class Converter extends Object {

  @mem fahrenheit( fahrenheit?: number ) {
    return fahrenheit ?? this.celsius() * 9 / 5 + 32
  }

  @mem celsius( celsius?: number ) {
    return celsius ?? ( this.fahrenheit() - 32 ) * 5 / 9
  }

}
Enter fullscreen mode Exit fullscreen mode
const conv = new Converter

conv.fahrenheit( 32 ) // 32 βœ…
conv.celsius() // 0 βœ…
Enter fullscreen mode Exit fullscreen mode

But there are two problems here. The first is infinite recursion if you access one of the properties without setting the value of at least one of them. With conventional methods, this would crash the browser, eat up a lot of memory, and crash with a stack overflow. But we have memoized methods - for a specific method called with specific keys in a specific object, they create a unique persistent atom. So, repeatedly accessing an atom while it is already being evaluated means an infinite recursion, which we can stop immediately, avoiding cyclic dependencies:

const conv = new Converter
conv.celsius() // Error: Circular subscription ❌
Enter fullscreen mode Exit fullscreen mode

At the same time, if the method calls differ in any way, then there is no infinite recursion. For example, we can give the classic recursive calculation of the Fibonacci number:

class Fibonacci extends Object {

  @mems static value( index ) {
    if( index < 2 ) return 1
    return this.value( index - 2 ) + this.value( index - 1 )
  }

}
Enter fullscreen mode Exit fullscreen mode

Thanks to memoization, even the thousandth number can be calculated instantly. But the price for this, of course, is the creation of thousands of caching atoms.

Another problem is two conflicting sources of truth. When we set the values of both states, both become primary and not necessarily consistent:

const conv = new Converter

conv.fahrenheit( 32 ) // 32 βœ…
conv.celsius() // 0 βœ…

conv.celsius(32) // 32 βœ…
conv.fahrenheit() // 32 ❌
Enter fullscreen mode Exit fullscreen mode

Correcting this code is not difficult - just move the source of truth into a separate property, and make both of ours derivative:


class Converter extends $mol_object2 {

  @mem source( value = { celsius: 0 } ) {
    return value
  }

  @mem fahrenheit( fahrenheit ) {
    const source = this.source( fahrenheit?.valueOf && { fahrenheit } )
    return source.fahrenheit ?? source.celsius * 9 / 5 + 32
  }

  @mem celsius( celsius ) {
    const source = this.source( celsius?.valueOf && { celsius } )
    return source.celsius ?? ( source.fahrenheit - 32 ) * 5 / 9
  }

}
Enter fullscreen mode Exit fullscreen mode
const conv = new Converter

conv.celsius() // 0 βœ…
conv.fahrenheit() // 32 βœ…

conv.fahrenheit( 0 ) // 0 βœ…
conv.celsius() // -18 βœ…

conv.celsius( 0 ) // 0 βœ…
conv.fahrenheit() // 32 βœ…
Enter fullscreen mode Exit fullscreen mode

However, combining sources of truth is not always possible.

Top comments (8)

Collapse
 
efpage profile image
Eckehard • Edited

Thank you much for your writeup. Impressive to see, how complicated things can get with the wrong approach. Wasn't React invented to make state management easier?

The reason, why the situation is so hard to solve, is a very general one. State based systems know, that the state has changes, but not, why the state has changed. But states does not change by their own, there is always a reason: something happened, that changed the state. We call this an "event". In the case of a Celsius to Fahrenheit converter, the user inputs a value in one of the two input fields.

With an event based approch, you will also get in trouble if you look only for state changes:

  • Celsius was changed, so update Farenheit
  • Farenheit was changed, so update Celsius.

But luckily, there are other events you can use. If you fire your event everytime a user inputs a value, things are pretty easy:

  • If the user inputs a value in Celsius, update Farenheit
  • If the user inputs in Farenheit, update Celsius

Event based systems can handle this kind of situations with the utmost ease.

IΒ΄m not telling you to use only event based logic everywhere. But it is important to see, that itΒ΄s a myth that state logic makes things easier. It depends much on the case.

I would prefer a mixed approach, where the "short range logic" (like updating a value if the neighbour changes) is handeled with an event based logic. Only golbal states with an influence of greater reach should be handeled by an explicit state logic. It's a real shame that this isn't possible with the React approach (as long as you do not put all the short range logic in webcomponents).

IΒ΄m sure, a hybrid approach would make many things much easier.

See this example

Collapse
 
ninjin profile image
Jin • Edited

React was invented just to write a PHP-style frontend.)

Events, due to their fundamental non-idempotence, are subject to a lot of problems related to handling exceptional situations, and timely (un)subscription, and preventing unnecessary calculations. I will soon prepare a separate article on the topic of detailed comparing reactive and event-driven architectures.

Collapse
 
efpage profile image
Eckehard

What do you think of the concept of a mixed approach using "short range" and "long range" reactivity? This could make things much simpler I suppose.

Thread Thread
 
ninjin profile image
Jin

This is a sure way to break the invariants and thereby make your life much more difficult. Especially when it comes to asynchronous invariants, which will be discussed in the next series.

Thread Thread
 
efpage profile image
Eckehard • Edited

Oh, yes, i forgot.... state management makes everyting ... easier?

Image description

Thread Thread
 
ninjin profile image
Jin
Thread Thread
 
efpage profile image
Eckehard

By the way: the page you are promoting in your profile (boosty.to/hyoo) cannot be switched to english anymore. Is this intentionally or is anything wrong with the state management ?

Thread Thread
 
ninjin profile image
Jin

I don't know, it's not my service. I have my own donation service in the plans.