The interface provides an abstract mean to compose computations with side effects.
You’ve already composed a lot of computations with side effects without seeing any Monad.
But in fact, you can consider you’ve already used them there.
In computer science, Monads first appeared for studying side effects in imperative languages.
They are a tool to embed imperative worlds into a pure math world for further studying.
This way if you want to convert your imperative program into math formulas representing it, doing this with Monad expressions would be the simplest and the most straightforward way.
It is so straightforward what you don’t even need to do it manually, there are tools that do it for you.
Haskell has a syntax sugar called do-notation exactly for this.
This makes writing imperative programs in Haskell possible.
There is a special tool in its compiler.
It converts such imperative programs into a Monadic pure Haskell expressions.
The expressions are close to math you see in textbooks.
We can consider any imperative code to be a do-notation already.
But unlike the one in Haskell’s, it is not abstract.
It works only for built-in side effects.
There is no way to add support of any new one except extending the language.
There are such extensions, namely generators, async and async generator functions.
JavaScipt JIT compiler converts async and generator functions into concrete built-in API calls.
Haskell doesn’t need such extensions.
Its compiler converts do-notation into abstract Monads interface function calls.
Here is an example of how async functions simplify sources.
This shows again why we need to bother having a syntax for effects.
Let’s call them Mutation and Exception.
They have clear meanings.
Mutations allow changing values of some references.
We can convert some effects into others.
This way we can write async code using Generators.
This conversion trick can be applied to other effects too.
And apparently, just Mutation and Exception are enough to get any other effect.
This means we can turn any plain function into an abstract do-notation already.
And this is exactly what Suspense does.
When the code encounters some effectful operation and requires suspension it throws an exception.
It contains some details (for example a Promise object).
One of its callers catches the exception, waits while the promise in the argument is settled, stores the resulting value in a cache, and re-runs the effectful function from the beginning.
After the Promise is resolved the engine calls the function again.
The execution goes from its start, and when it encounters the same operations it returns its value from the cache.
It doesn’t throw an exception and continues execution until the next suspension request or the function’s exit.
If the function doesn’t have any other side effects its execution should go the same paths and all pure expressions are recalculated producing the same values.
Let’s re-implement Suspense.
Unlike React, this one works with the abstract Monads interface.
For simplicity, my implementation also hides a resource cache.
Instead, the runner function counts invoked effects and uses the current counter value as a key for the internal cache.
Here is the runner for the abstract interface:Converting Exception and Mutation into abstract Monad interfaceNow let’s add a concrete Async effects implementation.
Promises, unfortunately, aren’t exactly monads since one Monad law doesn’t hold for them, and it is a source of subtle problems, but they are still fine for our do-notation to work.
Here is concrete Async effect implementation:And here’s a simple example, it waits for delayed values before rendering proceeds:The sandbox also contains Component wrapper.
It turns an effectful functional component into a React component.
It simply adds chain callback and updates the state accordingly.
This version doesn’t have a fallback on threshold feature yet, but the last example here does have it.
The runner is abstract, so we can apply it for something else.
Let’s try this for the useState hook.
It is a Continuation monad, not a State monad as its name may suggest.
Effectful value here is a function which takes a callback as an argument.
This callback is called when the runner has some value to pass further.
For example when the callback returned from useState is called.
Here, for simplicity, I use single callback continuations.
Promises have one more continuation for failure propagation.
And here is a working usage example, with most of “kit.
js” copy-pasted, except the monad’s definition.
Unfortunately, this is not exactly the useState hook from React yet, and the next section shows why.
Applicative do-notationThere is another extension for do-notation in Haskell.
It targets not only Monad abstract interface calls but also calls of Applicative Functors abstract interface.
Applicative interfaces shares the of function with Monads and there is another function, let’s call it join.
It takes an array of effectful values and returns a single effectful value resolving to an array.
The resulting array contains all the values to which each element of the argument array was resolved.
I use a different one from Haskell’s interface.
Both are equal though — it is simple to convert Haskell’s interface into the one used here and back.
This means we don’t need to write a concrete implementation of Applicative interface, we can generate it automatically.
If there is a default implementation, why do we need Applicative Functors?.There are two reasons.
The first one is not all Applicative Functors are Monads, so there is no chain method from which we can generate join.
Another reason is, even if there is chain, custom join implementation can do the same thing in a different way, probably more efficiently.
For example, fetching resources in parallel rather than sequentially.
There is an instance of this interface for Promises in the standard runtime.
It is Promise.
all(ignoring some details here for simplicity again).
Let’s now return to the state example.
What if we add another counter in the component?The second counter now resets its value when the first one is incremented.
It is not how Hooks are supposed to work.
Both counters should keep their values and work in parallel.
This happens because each continuation invocation erases everything after it in the code.
When the first counter changes its value the whole next continuation is re-started from the beginning.
And there, the second counter value is 0 again.
In the run function implementation, the invalidation happens at line 26 — trace.
length = pos — this removes all the memorized values after the current one (at pos).
Instead, we could try to diff/patch the trace instead.
It would be an instance of Adaptive Monad used for incremental computations.
MobX and similar libraries are very similar to this.
If we invoke effectful operations only from a function’s top level, there are no branches or loops.
Everything will be merged well overwriting the values on the corresponding positions, and this is exactly what Hooks do.
Try to remove the line in the code sandbox for two counters above.
Transpiler alternativeUsing Hooks already makes programs more succinct, reusable and readable.
Imagine what you could do if there were no limitations (Rules of Hooks).
The limitations are due to runtime-only embedding.
We can remove these limitations by means of a transpiler.
JS is a transpiler for embedding effectful into JavaScipt.
It supports both Monadic and Applicative targets.
It greatly simplifies programs in the designing, implementing, testing, and maintaining stages.
Unlike React Hooks and Suspense, the transpiler doesn’t need to follow any rules.
It never re-plays functions from the beginning.
This is faster.
JS is not exactly a transpiler but rather a tool to create transpilers.
There are also a few predefined ones and a lot of options for tuning.
It supports double-level syntax, with special markers for effectful values (like awaitexpressions in async functions, or Haskell’s do).
And it also supports a single level syntax where this information is implicit (like Suspense, Hooks or languages with Algebraic Effects).
I’ve quickly built a Hooks-like transpiler for demo-purposes — @effectful/react-do.
Calling a function with names starting with “use” is considered effectful.
Functions are transpiled only if their name starts with “use” or they have “component” or “effectful” block directive (a string at the beginning of the function).
There are also “par” and “seq” block-level directives to switch between applicative and monadic targets.
With “par” mode enabled the compiler analyzes variable dependencies and injects join instead of chain if possible.
Here is the example with two counters, but now adapted with the transpiler:For demo purposes, it also implements Suspense for Code Splitting.
The whole function is six lines long.
Check it out in the runtime implementation @effectful/react-do/main.
In the next example, I’ve added another counter which rendering is artificially delayed for demo purposes.
Algebraic EffectsAlgebraic Effects are often mentioned along with Suspense and Hooks.
These may be internals details or a modeling tool, but React doesn’t ship Algebraic Effects to its userland anyway.
With access to Algebraic Effects, users could override operations behavior by using own Effect Handler.
This works like exceptions with an ability to resume a computation after throw.
Say, some library function throws an exception if some file doesn’t exist.
Any caller function can override how it can handle it, either ignore or exit process, etc.
EffectfulJS doesn’t have built-in Algebraic Effects.
But their implementation is a tiny runtime library on top of continuations or free monads.
Invoking a continuation also erases everything after the corresponding throw.
There is also special syntax and typing rules to get Applicative (and Arrows) API — Algebraic Effects and Effect Handlers for Idioms and Arrows.
Unline Applicative-do this prohibits using any anything which requires Monad operations.
Wrapping upThe transpiler is a burden, and it has its own usage cost.
Like for any other tool, use it only if this cost is smaller than the value you get.
And you can achieve a lot with EffectfulJS.
It is useful for projects with complex business logic.
Any complex workflow can be a simple maintainable script.
As an example, Effectful.
JS can replace Suspense, Hooks, Context, and Components State with tiny functions.
Error Boundaries are the usual try-catch statements.
Async rendering is an async scheduler.
But we can use it for any computations, not only for rendering.
There are a lot of other awesome application-specific uses, and I’m going to write more about them soon.