The IO monad in Javascript — How does it compare to other techniques?

Magnus Tovslid
11 min readNov 24, 2019

--

In this article I will explore monads, and in particular the IO monad in Javascript (or Typescript). I want to figure out how it compares to other approaches like functional core imperative shell, dependency injection, and techniques we use in languages that are not purely functional. I’ll describe the subject as I understand it, and I don’t claim to be an expert in any of it. In fact, this whole thing is just a quest to try and understand how to make better programs. If I got anything wrong, please let me know!

This article is sort of a follow up to my previous article on “functional core, imperative shell”. You may want to read it as well:

https://medium.com/@magnusjt/functional-core-imperative-shell-in-javascript-29bef2353ac2

Note: I’ll use typescript in my examples. Hopefully that will be easier to understand than the Haskell type system.

Let’s start by looking at monads in general

What is a monad?

In short, a monad is anything that can be mapped and flatMapped (though map can always be derived in terms of flatMap, so is not as important)

  • map is a function that takes a function (val: T) => T, and returns a new monad (of the same type as what you called map on).

In other words, map lets you change the thing that lives inside the monad.

  • flatMap is a function that takes a function (val: T) => Monad<T>, and returns a new monad (of the same type as what you called flatMap on).

So flatMap is just a map followed by a flat, where flat unpacks the monad you returned.

Examples

An array is (almost) a monad:

[1, 2, 3].map(x => x * 2) // [2, 4, 6]
[1, 2, 3].flatMap(x => [x, 10]) // [1, 10, 2, 10, 3, 10]

It’s “almost” a monad because map and flatMap supports more than one argument, but that’s not important right now.

Another example that is (almost) a monad is a promise:

// map
Promise.resolve(5)
.then(x => x * 2) // Promise of 10

// flatMap
Promise.resolve(5)
.then(x => Promise.resolve(x * 2)) // Promise of 10

It’s “almost” a monad because

  • flatMap is called “then”
  • “then” also does the job of map
  • “then” can take more than one argument

But in essence it’s a monad.

Fun fact:

When the promise specification was worked out, some people argued that promises should be proper monads (https://github.com/promises-aplus/promises-spec/issues/94), but they were shut down and told they were living in a fantasy land.

Fantasy land is now a real project:

https://github.com/fantasyland/fantasy-land

What is a monad, really?

Ok, there’s a bit more to it. A monad has to be created somehow, so we need some function that takes a value and gives us a monad. We’ll call this function “of”. In the case of array, “of” is just the brackets [x] with one value inside. In the case of promise, “of” is just Promise.resolve.

If you read about monads elsewhere, keep in mind that the names of things can be different.

  • of is also called point, pure, return, etc
  • flatMap is also called chain, bind, etc

A monad also has to follow the monad-laws (see https://miklos-martin.github.io/learn/fp/2016/03/10/monad-laws-for-regular-developers.html).

Here’s a short description (feel free to gloss over this).

1. Left identity: If you create a monad with “of” and run flatMap(f), you should get the same result as just running f.

const f = x => [x, 10][1].flatMap(f) 
// should equal
f(1)

2. Right identity: If you have a monad and run flatMap(of), you get back the same monad

[1, 2, 3].flatMap(x => [x]) 
// should equal
[1, 2, 3]

3. Associativity: It should not matter how flatMap’s are nested.

[1, 2, 3].flatMap(x => [x, 10]).flatMap(x => [x, 20])
// should equal
[1, 2, 3].flatMap(x => [x, 10].flatMap(x => [x, 20]))

OK seriously, what is a monad?

There’s really nothing else to it. A monad is just a specification of the behaviour of some functions. What those functions actually do is completely up to you.

OK… So how is it useful?

Now this is an interesting question. A monad is useful because:

  • It’s a standard way of doing things. A monad of any type can be operated upon by anything that knows the monad laws.
  • A monad hides something from you. This can be anything, such as control flow, errors, side effects, state, environment, etc.

But I heard that monads are super important for functional programming. What’s the deal?

You don’t actually need monads for functional programming. But they certainly make some things easier in certain languages.

In particular, the thing most people seem to talk about when they talk about monads is the IO monad. The goal of the IO monad was to perform input/output in a non-strict, purely functional language like Haskell.

But are monads the only way to achieve this goal? No! Haskell didn’t always have the IO monad (it used something called Dialogues).

I recommend reading this paper on the IO monad for some context:

https://www.microsoft.com/en-us/research/wp-content/uploads/1993/01/imperative.pdf

Another approach to input/output in a pure language is continuations (think: callbacks). Here’s what the authors of the IO monad paper thought about continuations:

“This extra argument is required for every I/O-performing function if it is to be composable, a pervasive and tire-some feature.”

In other words, they wanted to get rid of callback hell (Hello Javascript community in 2015!). They did this using monads (Just as the Javascript community sort-of did with promises).

So, since Haskell figured out how to get rid of callback hell back in 1993, maybe we should take a closer look at their solution, the IO monad.

The IO monad

By now we sort of know what a monad is, but what makes a monad an IO monad specifically? We know it’s to do with input/output somehow, or in other words: effects.

An effect is just something should happen (and it should not not happen), and may be impure.

Why is it so important that it should happen? That’s because in a non-strict, purely functional language, like Haskell, evaluation of things is lazy.
Also, since everything is pure, Haskell is free to memoize the result of any function (in theory at least, in practice this leads to memory leaks).
This does not work well with effects, since they may happen in the wrong order, may only happen once even though they were called several times, or may not happen at all.
The IO monad solves these issues. However: Note that we don’t have these issues in most other programming languages, and so we don’t need to go through as many hoops as Haskell. Also, this may mean that the IO monad is not as useful in most languages.

In Haskell, an IO effect is defined something like this (converted to typescript):

(world: RealWorld) => [world: RealWorld, value: T]

So just a function that takes in the “real world”, returns a new world and some value that was the result of the effect. The “real world” is actually only used by the type system to ensure that effects are run, and run in the proper order. The world value disappears during compilation.

We don’t need the guarantees the world value gives in Haskell, so we can just omit it, and define an effect as:

() => T

So it’s just a function that returns a value (a “value” can also be void).

Now, to make a monad out of this we need:

  • of
  • flatMap
  • Some convenience, for example a class IO

Here’s my first attempt at an IO monad in typescript:

import fs from 'fs'

type Effect<T> = () => T

class IO<A>{
private effect: Effect<A>
constructor(effect: Effect<A>){
this.effect = effect
}
static of<T>(val: T){
return new IO(() => val)
}
map<B>(f: (val: A) => B): IO<B>{
return new IO(() => f(this.effect()))
}
flatMap<B>(f: (val: A) => IO<B>): IO<B>{
return new IO(() => f(this.effect()).effect())
}
eval(){
return this.effect()
}
}

As you can see, we have the methods of, map, and flatMap as described earlier. The “value” inside the monad is an effect. We also have an “eval” function that actually runs all the effects.

We can use the IO monad to read and write to some files:

const readFile = (fileName, opts?) => 
new IO(() => fs.readFileSync(fileName, opts))

const writeFile = (fileName, data, opts?) =>
new IO(() => fs.writeFileSync(fileName, data, opts))

const program = readFile('monad.txt', 'utf8')
.map(content => content + ' more content')
.flatMap(content => writeFile('monad2.txt', content, 'utf8'))
.flatMap(() => readFile('monad2.txt', 'utf8'))

const result = program.eval()
console.log(result)

Here we have two effects inside readFile and writeFile. We also have our “program”, which is itself an IO monad. It is not until we call eval() on our program that the effects are run, so before the call to eval, we have a completely pure program. We can still write our program as if we’re calling impure functions, even if we’re actually not.

There’s a few issues though:

  • How do we test this? Even though read/write file is not run before the call to eval, we would perhaps like to mock them for a test
  • We’re using sync versions of fs. We should be using async
  • The syntax is not very nice. We want something like async await

Luckily we can fix all of this. Let’s start by adding some syntax sugar similar to async await. This is what’s called do-notation in Haskell.

Do notation

const _do = (fn: (...args: any[]) => Generator) => (...args) => {
const gen = fn(...args)

const next = (val?) => {
const res = gen.next(val)
if(!res.done) return res.value.flatMap(next)
if(res.value && res.value.flatMap) return res.value
return IO.of(res.value)
}

return next()
}

const program2 = _do(function * (){
let content = yield readFile('monad.txt', 'utf8')
content = content + ' more content'
yield writeFile('monad2.txt', content, 'utf8')
return readFile('monad2.txt', 'utf8')
})

const result2 = program2().eval()
console.log(result2)

We have to use generators, but this is almost the same syntax as async await.

Here’s something to ponder: If this “pure” program looks just like the impure counterpart, how can it be better?

(Note: This “do”-notation that I just made up doesn’t work for all monads unfortunately. Generators are not powerful enough for that. See: https://github.com/pelotom/burrido).

Testing

Now let’s do something about testing. Let’s inject the effects instead of running them directly. Effects are now defined as (effects: Effects) => T instead of just () => T.

Here is our new IO monad, with effects injected:

import fs from 'fs'

type Effects = {
readFile: (fileName, opts?) => string
writeFile: (fileName, data, opts?) => void
}

type Effect<T> = (effects: Effects) => T

class IO<A>{
private effect: Effect<A>
constructor(effect: Effect<A>){
this.effect = effect
}
static of<T>(val: T){
return new IO(() => val)
}
map<B>(f: (val: A) => B): IO<B>{
return new IO((effects) => f(this.effect(effects)))
}
flatMap<B>(f: (val: A) => IO<B>): IO<B>{
return new IO((effects) => f(this.effect(effects)).effect(effects))
}
eval(effects: Effects){
return this.effect(effects)
}
}

We can now write our program like this (Note that program2 is unchanged):

const readFile = (fileName, opts?) =>
new IO((effects: Effects) => effects.readFile(fileName, opts))

const writeFile = (fileName, data, opts?) =>
new IO((effects: Effects) => effects.writeFile(fileName, data, opts))

const program2 = _do(function * (){
let content = yield readFile('monad.txt', 'utf8')
content = content + ' more content'
yield writeFile('monad2.txt', content, 'utf8')
return readFile('monad2.txt', 'utf8')
})

const effects: Effects = {
readFile: (fileName, opts?) =>
fs.readFileSync(fileName, opts).toString(),

writeFile: (fileName, data, opts?) =>
fs.writeFileSync(fileName, data, opts)
}

const result2 = program2().eval(effects)
console.log(result2)

We now have the ability to send in our unsafe effects when we make the call to eval. This enables us to make pure versions of our effects for testing - but although we have the ability to test, it’s not necessarily easy to test.

By sending in our effects like this, combined with do-notation, we enable something that resembles algebraic effects as well (see: https://overreacted.io/algebraic-effects-for-the-rest-of-us/)

Supporting async effects

The final thing we wanted to fix was to use async versions of the fs api’s. We can do this already since our effects can return any value, including promises. It wouldn’t be very nice, though - we would have to await after yielding. It would be better if our IO monad supported promises directly. This is not hard to add, but I’ll skip over it in this article, as I don’t think it changes much.

Tying it all together

What did we achieve with the IO monad?

  • We wrote a program where every part of the program was pure (except for the leaf node effects and the top node eval). We could call any function in such a program without having side effects run, except for a single call to eval to kick everything off.
  • We used a technique (the monad) that could have been used for many other things as well. Any function we could write for dealing with monads would be useful for many other monads as well.
  • We could perform dependency injection of our effects. Note also that we could have done this in many other ways. We could have let the “effects” simply describe the effects instead of running them. It would be possible to actually run the effects in an entirely different place. An interpreter of sorts.

What did we NOT achieve?

  • Even though our program is pure in a sense, it is not more predictable. We did not handle a single exception in our example code, nor did the monad approach force us to handle them. In fact, our program looks remarkably similar to the one we would write without monads. Also, if we simply wrote a normal, imperative program, and wrapped the entire thing in a function, we would achieve the same thing! So obviously we have to do more if we want more value out of the IO monad.

Comparing it to dependency injection

It seems that the IO monad is agnostic to dependency injection. We can still do it, or we can choose not to. The thing that is interesting to me is that we can now do the dependency injection after creating the program, not before. That is, we don’t need to pre-bind functions with their dependencies and pass them down into other functions, we can now compose functions unaware of their dependencies. This is possible because the IO monad abstracts away the running part of running the effects.

Comparing it to functional core, imperative shell

In a previous article I wrote, I describe an approach to functional programming that I like a lot, called functional core, imperative shell. The gist is that you write as much pure functions as you can, and sandwich those in between impure code. This gives us the many benefits of pure functions; predictability and testability in particular. In addition, the remaining “imperative shell” is smaller, with less logic, and less need for dependency injection.

In a way, the IO monad let’s us write the entire program as a functional core (since everything is now pure), so that’s what we should be doing, right? Well, the thing is that even though things are technically pure, we don’t get any of the benefits of it being pure out of the box. The program is not any more predictable or testable unless we do something more than just use an IO monad. The main issue is that the so-called pure code dictates when and how the impure code is run. It must then make sure to handle errors like normal, and we must use dependency injection as we otherwise would.

To sum up, I would not consider the IO monad as part of a functional core.

Conclusion

The IO monad is an interesting technique that solves real problems in Haskell. In other languages, it’s not so clear. It seems we must still do dependency injection, handle exceptions, and separate pure functions into a functional core. In fact, the code we end up writing looks remarkably similar to the imperative counterpart.

As far as I can tell, the main benefit to the IO monad in a language like Javascript is the ability to compose programs in a different way, in particular the way in which we can do dependency injection without passing functions (or classes) as parameters to intermediate functions. Note also how this resembles the algebraic effects described here: https://overreacted.io/algebraic-effects-for-the-rest-of-us/

The main downside of the IO monad is pretty obvious. It is not idiomatic to javascript. This is a major problem, because you can’t just stick in an IO monad here and there. It’s sort of the foundation on which the entire program is built (or at least large portions of it).

Do you have any experience with monads in Javscript/Typescript? Please let me know in the comments!

--

--

Responses (5)