Monads in JS

The idea to understand the topic of monads has attracted me for a very long time. The complexity of describing concepts was not only a personal problem for me, but also a potential problem for colleagues. After all, I wanted not only to understand them, but to work with them every day. Functional programming is a good form of thinking, it is a very expressive and often concise solution. Below is a description of the development experience using monad libraries on JS / TS

First experience

I managed to make the first run on monads on a small project local to the company, which did not require daily support and team interaction. In a sense, it was a small pet-project of a company named after me alone.

The idea was to make it as simple as possible to enter only numbers from 1 to 99

In addition to the extraneous routine, the input handler contains, in fact, the “transformer-validator” of the entered value itself:

const numericValue = Maybe.of(event.target.value)
  .filter(isString)
  .map(leaveOnlyDigits)
  .map(parseToDecimals)
  .filter(notNaN)
  .map(invertIfNegative)
  .map(limitTo99)
  .map(makeOneIfZero)
  .getOrElse(1);

In this example, the Maybe monad from the library was used folktale. There is every reason to believe that monads from other libraries will essentially work the same way. Folktale itself has not been developed for many years.

In this example, the monad wraps the value of the input field and then performs checks and transformations with pure, simple functions, each of which is easy to write and test. The naming of functions in the field speaks for itself, so there is no point in citing their code.

As a result, we get a readable and understandable chain of interactions with the value of the input field. Profit!

For those who are not familiar with this concept, I would like to make a small theoretical digression and place a few accents that I think are appropriate in this context. Without any doubt, more complete information can be found in the documentation for the libraries, and even Runet is rich in theoretical calculations on this topic.

So the monad Maybe can be of two subtypes Just or Nothing.

Monad Creation Method Maybe.of returns Maybe.JustA that contains the value passed to the method. It can be anything, incl. undefined or null. When creating this monad, we still do not care what this value will be.

Method map also returns Maybe.Justbut with the value already changed by the function that we are in this method, provided that our monad has a subtype Maybe.Just. If you call the method map at the monad of the subtype Maybe. Nothingno action will be taken and a monad of type Maybe. Nothing.

Method filter returns Maybe.Justif the function passed to this method, when interacting with a value inside the monad, will return true. Otherwise, the method will return the monad Maybe. Nothing

Method getOrElse returns the value of the subtype monad Maybe.Justor returns the value passed to this method.

I must say that this particular chain of transformations and checks can be presented in a more familiar form for javascript developers:

const numericValue = [event.target.value]
  .filter(isString)
  .map(leaveOnlyDigits)
  .map(parseToDecimals)
  .filter(notNaN)
  .map(invertIfNegative)
  .map(limitTo99)
  .map(makeOneIfZero)
  .find(it => it) || 1;

Such a feint is possible, since arrays in JavaScript are full-fledged functors. That is, they have a method in this case map, which takes some conversion function as input and returns an object of the same type as the output, that is, the same array. But inside it already contains the modified value.

So, it is quite possible to write similar, simple pseudo-monadic things on ordinary arrays.

Experience in a fully practical area

Then there was an attempt to drag the whole thing to the back. But with the library folktale this turned out to be impossible due to the lack of implementation of work by asynchronous methods, and at that time it was decided to limit ourselves to a pseudo-monad solution. But by that time it became clear how convenient it would be to work with such an approach.

As a result, when the opportunity presented itself to start a project in TypeScript, we managed to find a library with monads that could handle asynchrony: purify-ts. The need for the appearance of monads in the project was due to the convenience of error handling. The monad was used for this. Either and its asynchronous counterpart EitherAsync. In addition to these monads, the library contains many others, incl. And Maybe. They are perfectly combined with each other and allow you to write in a functional style. By the way, if suddenly a developer who will work with this approach suddenly encounters difficulties, one must understand that the monad can always be “deployed” and continue working with the code as usual. Of course, all the charm of the functional approach and its expressiveness and conciseness will be lost.

The following is an example of a method for getting cached user data and updating it when the cache has expired. If it suddenly turned out that when updating the data, the user’s status became invalid, we log out the user from the system.

return EitherAsync.liftEither(this.stateManager.checkUserExpiration())
  .bimap(
    // если дата истекла
    () => EitherAsync.liftEither(this.stateManager.getUser())
      .map((user) => user.id)
      .chain((userId) => this.userService.getUser(userId ?? ''))
      .map((user) => this.stateManager.setUser(user)
        .chain(() => this.stateManager.setUserExpiration())
        .reduce((acc) => acc, user))
      .chain((user) => EitherAsync.liftEither(this.stateManager.checkUserStatus())
        .map(() => user)
        .chainLeft(async (statusError) => (await this.authService.removeAllUserSessions(user.id))
          .chain(() => this.stateManager.logout())
          .map(() => statusError),
        ),
      )
      .then((either) => either.extract()),

    // если дата НЕ истекла
    () => this.stateManager.getUser().extract(),
  )
  .run()
  .then((either) => either.extract());

There is a slightly different naming of methods, but the essence is exactly the same.

It is worth paying attention to the method chainwhich takes as input a method that returns a monad of another type (let’s call it Monad2). He “digests” it and returns a new monad. The operation of this method can be compared with the method flatMap arrays. If we had used the map method instead of chain, we would have returned the original monad (Monad1), the value of which would be Monad2and so we will immediately have Monad2.

Once again in other words:

Monad1.map(() => Monad2): => Monad1[Monad2]
Monad1.chain(() => Monad2): => Monad2

Array.map(() => Array) => Array[Array]
Array.flatMap(() => Array) => Array

Monad Either, like Maybe can be of two types, but in her context they are called Left (for incorrect values) and Right (for correct ones). This makes its use in error handling very useful. She looks a bit like Maybe with the exception that Maybe. Nothing there can be no value, but in Either.Left Maybe.

Method bimap is a convenient method for simultaneously working with monads of type Either. This is a simultaneous mapper for Either.Left And Either.Right monads. If, as a result of checking

EitherAsync.liftEither(this.stateManager.checkUserExpiration())

we get a monad Either.Rightwe will move on to the mapper for Either.Right:

() => this.stateManager.getUser().extract()

Otherwise, we have to go through a long chain of checks, requests and transformations from the mapper for Either.Left.

The result of the work of this method (the one that is large, above) will no longer be a monad, but an object of a specific type: NetworkError (this is what we had in the monad Either.Left), or User (this is what we had in the monad Either.Right). This object further goes to the request handler. In our case, I worked with Express:

sendResult(res: Response, result: any) {
  if (result instanceof NetworkError) {
    return res.status(result.status).send(result.error).end();
  }

  res.json(result || 'OK');
}

NB: The project used tsoa to generate the swagger specification, so I had to expand the monad here. Otherwise, it could be thrown right up to the handler itself sendResult and send the response also in functional style. It would be more elegant.

NB: Working with this library is somewhat reminiscent of the syntax for working with the Java Reactor. In general, if you rebuild your brain a little and abstract, you get quite expressive code.


In conclusion, I would like to say that this experience was very exciting! Thinking about, understanding and understanding the details and various theoretical and practical aspects of monads and functional programming is quite difficult, but it trains the brain, expanding the understanding of programming in general, gives the practice of a completely different approach to js within the framework of familiar tools. Well, if you do everything consciously and carefully, you get a supported and testable code. It’s clear that you can code for ***** by any means, incl. and functional style. But as they say: “Do it right, it will be fine”

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *