Why abstract interface for effects matters

JavaScript recently received three new computational effects kinds. There are:

  • generator functions
  • async functions
  • async generator functions

The latest is a combination of the first two. If at some point JavaScript gets some other effect extension in syntax it will have to have seven language extensions for each combination, and so on. This may be, for example, recently added in runtime Observable effects for reactive programming. It may get own syntax extension sooner or later.

The latest extension, namely async generators, isn’t without problems. There is no way now to cancel async computation in JS now. This means it is impossible to combine async iterators as easily as Observables without introducing leaks. There is a proposal to add the cancellation, but it is still on too early stage and will probably take years. Alternatively, you may use Effectful.js immediately without waiting.

Libraries should be ready for abstract interface too. For example, higher order functions, like Array traversal functions (Array#map, Array#forEach, etc.). Say, we have a function duplicating elements implemented as a simple generator function:

We cannot just copy-paste the body of the loop into, say, forEach body like this:

To make a fully general library we need four versions of Array#forEach, and each other higher order function: Array#map, Array#filter, etc. Say, filter predicate may need to consult some remote service and so it should be async.

It is even worse if the task like AST transforms with a significant number of nodes and Visitors written for traversal. To make a generic library there should be four versions of visitor types again.

There is an abstract interface (Monad) for effects. Only one implementation for the functions above is needed if it uses such abstract interface to build result.

The Monad interface came from math, namely category theory. Its only purpose was an abstraction. Theorems proved using abstract monads interface may be immediately applied to likely also abstract but more concrete interface instances, like some structures from universal algebra or topology. Mathematicians refactored math frameworks by abstracting common things into Category Theory and Monad as a part of it.

Later the interface was utilized to describe and reason about programming languages with effects in domains theory. The same purpose again, simplifying reasoning about programming languages and programs.

After, Monads reached practical programming languages. Other researchers worked on programming languages with only pure functions to simplify reasoning about programs. Unfortunately having only pure function requires threading functions parameters and results, result statuses, etc. This makes programs very verbose and hard to read. The problem was solved by applying Monads. Imperative code with a mutable state, exceptions can be converted into a pure function.

Applying Curry–Howard correspondence, where programming language types are theorems, and programs are their proofs, Monads are abstract API. So like in math, proved theorems with some general math structure may be applied to any concrete realization of that structure. In programming languages, a function using abstract type for arguments objects may be invoked with any concrete implementation of that object.

There are many different options in choosing names for interface functions, or a set of basis functions, or split it into a hierarchy of other abstract notions somehow. There are a few libraries in JavaScript already defining the interface and providing a few of its implementations. Like fantasy-land.

Effectful.js compiler uses own interface, but it may be adapted to any other either by implementing wrappers or inlining calls to other methods. It doesn’t introduce any syntax extensions, but can overload JavaScript ones, e.g., generators or await/async syntax to generalize them.

Many custom effects or changes/fixes for ECMAScript can be applied immediately without awaiting committee for years. For example:

  • cancelation to async functions, thus making async iterators compasable
  • improve performance (drop some not-efficient and useless standard feature, remove JavaScript engine scheduler from async iterator)
  • persistent state (time traveling, distributed apps, workflows)
  • non-deterministic programming (bind form’s data as logical formulas, reactive programming)