DEV Community

Cover image for Reactive Dataflow Configurations
Jin
Jin

Posted on • Originally published at page.hyoo.ru

Reactive Dataflow Configurations

In a reactive system, all states are connected to each other by invariants into a single graph. When we change something on one side of this graph, runtime provides a cascade recalculation of dependent states. Such sequences of recalculations are nothing more than information flows (data flow). The more straightforward these flows are, the less they branch out and affect states that are irrelevant to changes, the more efficiently the system operates. And here there are two approaches to optimizing information flows..

🦽 Manual
🚕 Auto

🦽Manual

In push-based libraries, it is difficult to automate flows, so manual management of them thrives. This means we get two types of errors...

Firstly, we may forget to subscribe to something, resulting in inconsistency. In the example, we forgot to subscribe to Title, and when it changes, Greeting is not recalculated.

Secondly, we may forget to unsubscribe from something, resulting in unnecessary calculations. In the example, we forgot to unsubscribe from Name, and when it changes, Greeting is recalculated, but gets the same value.

But, if you can still somehow cope with errors, then it is no longer easy to cope with the complexity of manual optimizations. For banal logical branching, we need to actually implement a transistor by hand, where we have a control flow that switches the output between two inputs. For loops and indirect addressing, everything becomes so complicated that few people are even able to adequately describe it. In the end, it all comes down to the fact that, instead of point-by-point recalculations, many states are recalculated for any sneeze, which is quite slow.

Look at this FRP rebus and try to immediately say what it does and why:

const ToysSource = new Rx.BehaviorSubject( [] )

const Toys = ToysSource.pipe(
  distinctUntilChanged(),
  debounceTime(0),
  shareReplay(1),
)

const FilterSource = new Rx.BehaviorSubject(
  toy => toy.count > 0
)

const Filter = FilterSource.pipe(
  distinctUntilChanged(),
  debounceTime(0),
  shareReplay(1),
)

const ToysFiltered = Filter.pipe(
  switchMap( filter => {
    if( !filter ) return Toys
    return Toys.pipe( map( toys => toys.filter( filter ) ) )
  } ),
  distinctUntilChanged(),
  debounceTime(0),
  shareReplay(1),
)
Enter fullscreen mode Exit fullscreen mode

And it does a simple thing: it creates a stream for products, a stream for filtering criteria, and from them receives a stream of a filtered list of products.

Several standard optimizations have already been applied here. However, this code does not work very efficiently: the list of toys is refiltered even if the data in the product has changed, on which the filtering result does not depend. To overcome this problem, you will have to complicate the code by an order of magnitude, but few people will cope with this.

This approach results in complex, hard-to-maintain code. It's difficult to read. It's difficult to write. He's too lazy to write correctly. And it’s easy to make a mistake, unless, of course, you are a finalist in the Special Olympiad in Informatics.

🚕Auto

Pull-based libraries typically use dependency auto-tracking. Not only is this much more reliable, it is also extremely simple for an application programmer. It does not need to think about data streams at all - they are dynamically configured by runtime in the most optimal (for a given application state) way.

Here application programmers are divided into two camps: some are afraid of this “magic” because they don’t understand how it works, while others simply don’t give a damn - it works and works, one less headache.

Well, on the sidelines stands the camp of those who simply know how it works and use this knowledge to benefit. After all, as you know: any sufficiently developed technology is indistinguishable from magic... for the uninitiated person.

Practice shows that (with automation) the application code is orders of magnitude smaller, the code itself is much simpler and more reliable, and the application runs faster.

class $my_toys {

  @mem toys( next = [] ){ return next }

  @mem filter(
    next = toy => toy.count() > 0
  ) { return next }

  @mem toys_filtered() {
    if( !this.filter() ) return this.toys()
    return this.toys().filter( this.filter() )
  }

}
Enter fullscreen mode Exit fullscreen mode

Isn't the ORP code much simpler and more understandable? This is the same code that we would write without any reactive programming, but we added a special decorator that dynamically tracks dependencies as they are accessed, caches the result of the function execution, and resets the cache when the dependencies change.

Correct implementation of the logic of these decorators allows calculations to be performed in the most optimal way, without shifting the headache of controlling data flows to the application programmer.

Similarly, we can add a sorted list of products, depending on the sorting function and the filtered list.

@mem sorter(
  next = ( a , b )=> b.price() - a.price()
) { return next }

@mem toys_sorted() {
  if( !this.sorter() ) return this.toys_filtered()
  return this.toys_filtered().toSorted( this.sorter() )
}
Enter fullscreen mode Exit fullscreen mode

By default, there is sorting by price, which means sorting will be done again only if the price of any toy from the filtered list, the filtered list itself, or the sorting criterion changes.

Top comments (0)