DEV Community

Cover image for Reactive Paradigms
Jin
Jin

Posted on • Originally published at page.hyoo.ru

Reactive Paradigms

Conventionally, there are 4 approaches to writing code.

🧐 Proc: Procedural
🤯 Func: Functional
😵 Cell: Cellular
🤓 Obj: Object

Different libraries can mix them in different proportions, but as a rule there is a clear inclination towards one of them.

🧐Proc

Here an update procedure is sporadically launched, which reads some states, calculates others and writes them. Let's write a simple, although not very effective, implementation...

let Name = 'Jin'
let Count
let short

setInterval( ()=> {
    Count = Name.length
} )

setInterval( ()=> {
    Short = Count < 4
} )
Enter fullscreen mode Exit fullscreen mode
let Short_last
setInterval( ()=> {
    if( Short === Short_last ) return
    console.log( Short )
    Short_last = Short
} )

Name = 'John'
// logs false
Enter fullscreen mode Exit fullscreen mode

Invariants are described approximately this way, for example, in Meteor and Angular by default. Of course, they do not start the recalculation every millisecond, but more optimally, but this does not change the general essence: runtime periodically restarts the invariants, not knowing which states can be changed by them. But the actual values of these states may not be interesting to us, but they will be calculated in any case. Therefore, this approach is still not very effective.

🤯 Func

In the wake of hype, many focus on pure functions, turning their code into a puzzle...

const Name = new BehaviorSubject( 'Jin' )

const Count = Name.pipe(
    map( Name => Name.length ),
    distinctUntilChanged(),
    debounceTime(0),
    shareReply(),
)

const Short = Count.pipe(
    map( Count => Count < 4 )
    distinctUntilChanged(),
    debounceTime(0),
    shareReply(),
)
Enter fullscreen mode Exit fullscreen mode
Short.subscribe( short => {
    console.log(short)
} )
// logs true

Name.next( 'John' )
// logs false
Enter fullscreen mode Exit fullscreen mode

Even an experienced streamer cannot immediately tell what this RxJS code does and why. But this is the simplest example, far from the real thing.

However, smart programmers love puzzles. So they spend a lot of time studying cunning abstractions that are equally distant from both how a machine works and how the human brain works. They write concise but intricate code. And they are proud that they understand what few others are able to understand. This has a rather negative effect on the project, introducing unnecessary complexity to an area that is already full of difficult things.

I used to write tricky code, too, but life taught me that it’s better to write the simplest code possible, accessible even to a beginner in programming, and not just to winners of computer science Olympiads.

In addition, the abundance of closures inherent in functional code leads to increased memory consumption.

😵 Cell

Some compromise between the functional and procedural approach is the approach with reactive cells (atoms, signals) - mutable containers, for one value each, connected to each other through closed functions.

const Name = observable( 'Jin' )

const Count = computed( ()=> {
    return Name().length
} )

const Short = computed( ()=> {
    return Count() < 4
} )
Enter fullscreen mode Exit fullscreen mode
const Autorun = autorun( ()=> {
    console.log( Short() )
} )
// logs true

Name.next( 'John' )
// logs false
Enter fullscreen mode Exit fullscreen mode

This may be due to the fact that reactive invariants are not required to be pure functions, but are required to be idempotent functions. That is, they can depend on a mutable state, but only if it is reactive.

Most modern reactive systems are made using this approach. Prominent representatives: CellX, MobX, WhatsUp.

Unfortunately, the problem with memory consumption is even more significant here, since several closures are created for each cell. In addition, this approach has difficulties with debugging, since there is no easy access through a debugger to the state isolated in the closure.

🤓 Obj

The issue of decomposition and discoverability is well covered by the object paradigm, where a program consists of many objects with states connected by invariants into a single graph. Code in this style looks the same as regular OOP code, but with the addition of reactive memoizers.

class State {

    @mem Name( next = 'Jin' ) {
        return next
    }

    @mem Count() {
        return this.Name().length
    }

    @mem Short() {
        return this.Count() < 4
    }

}
Enter fullscreen mode Exit fullscreen mode
class App {

    @mem State() {
        return new State
    }

    @mem Logging() {
        console.log( this.Short() )
    }

}

const app = new App
app.Logging()
// logs true

app.name( 'John' )
// logs false
Enter fullscreen mode Exit fullscreen mode

Many people have probably heard the statement that “cache invalidation is one of the most difficult issues in programming.” So, in reactive runtime, such a question does not arise at all.

This approach seems to me to be the most optimal, since it fits well with the way a person thinks (and he is accustomed to interacting with objects) and with the way a computer works (an object is simply a mutable structure in memory). Runtime clearly understands which method calculates which state. And object decomposition makes it all easy to scale. That is why the object style is used in $mol_wire as the main one.

Top comments (0)