DEV Community

Dave Cridland
Dave Cridland

Posted on

Multi-Paradigm is the best Paradigm

I've been learning React recently - my first bit of Javascript programming in something like two decades. It's been fun.

React's JSX gives you declarative code to express the structure of your application. Redux gives you imperative, functional-like techniques to manage state. Saga throws imperative coroutines into the mix. Given this delightful framework, you can then crack on with writing imperative procedural code in an object-oriented language to get stuff done, switching back and forth between procedural and OO as you feel.

There's much to recognise from several years of messing about with code, and it's nice to see it aligns with my thesis in the title - that sticking religiously to a single paradigm is overly restrictive and probably blocking the right way to do the task at hand.

Warning: Paradigm Shift Lock is on

Paradigms - whether your environment is Declarative or Imperative, Functional or OO - impose restrictions on how you express a problem, and therefore how you have to think about it. These restrictions are there in order to lower your cognitive load, and (usually) forget about the detail of one area while working in another.

Declarative code says how things shall be, and avoids discussing how they'll get there. Pure functional code is a form of Declarative code, but there are perhaps better examples - SQL and XSLT, for example. While it's very popular for these types of domain-specific languages, it's never really taken off as a general purpose programming technique - despite the brave efforts of Haskell and others.

Imperative code, on the other hand, discusses only a sequence of operations - a list of instructions. It turns out this fits the human mental model for complex tasks much better than a declarative model. Consider a recipe for a cake - describing that the cake is covered in icing and has jam in the middle is a wonderful thing for making me hungry, but doesn't tell me much about how I'm supposed to make it. So it is with a large application.

Imperative code then splits fairly neatly into Procedural code - where there are functions and you call them, basically - and Object Oriented - which is perhaps the most misunderstood paradigm of them all.

OO is based around a fairly simple concept: That state and behaviour are bound together as an object, which is specialised for its task, and you can only invoke behaviour indirectly by passing a message with defined semantics. All of which sounds arcane compared to most OO languages, so let me explain that the message passing is usually implemented as method calls. Please note that I haven't said "class", or "inheritance".

Almost every modern imperative language is OO, and this includes ES3, Lua, and countless other languages which people have spent many an evening insisting are not OO. Seriously, if there's one thing to take away from this section, it is that OO isn't just that thing Java does.

In all cases, programming paradigms are there to assist the human brain is reasoning about, and working with, complex applications. Mostly, this is achieved by enabling you to make assumptions about behaviour based on a small set of general rules, localising complexity.

Reactionary Paradigms

At the top-level, React is a Declarative language, via JSX. Each component is simply declared, and exactly how it comes into being with its properties and state is largely hidden away. The application as a whole is cast into existence directly from the void - there is almost no initialisation, no bootstrapping, and no boilerplate. As a way to describe the structure of the application and avoid the pitfalls of shared state across different portions of the application, it's pretty much as awesome as it can be.

When it comes to defining components, though, React reaches for Imperative code. But - and I think this is truly clever - it avoids diving directly into full-blown OO. Instead, it leans toward the procedural, by allowing simple components to be defined as functions.

Now, lest my comment stream be filled with "Oh but really", yes, these do define objects - but the syntax is that of a function definition.

Still, React does have a very full ES6-style class system available for more complex components, yea, even unto the inheritance tree, and very useful it is, too.

Reduce, Reuse, Recycle

When managing state, most React developers seem to turn to Redux, and its friend, Ducks.

The global rule of state is that you should have no global state - that's really a matter of managing expectations with side-effects of code calls, as we know - and Redux approaches this very neatly indeed.

It provides a mahoosive global state object, but treats it as being read-only - like the React components' properties, in fact, which often bind to a portion of the state. These portions are managed using individual Ducks, which Redux combines into a mashoosive "Reducer". (Aside: "Mashoosive" was a typo, but I really like it, so it's staying).

Other parts of the application which need to change this state do so indirectly by sending JSON-like objects to Redux, which dispatches them to various Ducks which encapsulate portions of state and implement their behaviour. If this sounds oddly familiar, it's because I was literally just using these terms to describe Object Oriented Programming.

But wait! Didn't I say it was "functional-like" in my introduction? Yes, I did, and it is. Redux borrows vast tracts of Functional programming in order to manage state, too. Viewed through this lens, the methods implemented by Ducks can be seen as Reducers (as in Map/Reduce). This is by far the more normal way of looking at Redux, which is why the state management functions are indeed called Reducers.

So instead of manipulating the enormous global state object, you always see this as the result of a function call, and instead of changing the state directly, reducers are called with a copy of the state and return the new state. Data is never changed.

But, if you prefer to call the reducers "methods", and think of Ducks as objects, I won't tell anyone.

Saga, Saga, Burning Bright

When managing really complex sequences of events, though, Redux isn't quite enough. While you can bend your mind around translating a sequence of events into a set of nominally independent handlers, it's really hard.

Saga gives some useful tools for this by wrapping things into co-routines. Co-routines are the little brother of threads, and are build around generators. Generators are a way of producing a function that returns a sequence created as you iterate through it. As a for-example, you can print an infinite Fibonacci sequence by writing a simple Fibonacci generator. It'll generate the next value each time the iteration runs through, essentially suspending execution of the function when it "yields" a value.

Co-routines are built by yielding in order to hand back control to a master scheduler, giving you co-operative multitasking between co-routines. In the case of Saga, the values yielded are Promises, and the Promise resolving causes execution to be resumed; this allows you to await events from external sources, dispatch Redux events, and so on - but the flow of control becomes visible (and manageable) as a single function.

Event-driven programming is already an Imperative process, of course, but by introducing co-routines it makes it a simple linear one, and therefore fits the mental model better.

When all you have isn't a Hammer

There's a temptation to think that whatever tool you have to hand is the best one for the job. None of us think that's really the case after a moment's thought, but I'm as guilty as anyone else for using a tool or technique because it was one I knew could solve the problem, even while I knew that another tool I didn't know as well would be a better fit.

The clever bit of React and its friends isn't that they have picked the One True Paradigm. The clever bit is that they've seamlessly melded several different paradigms together and made them work effectively.

Oldest comments (0)