By Daniel Weiner, Software Engineer, Breakthrough Technologies
In this article I'll talk about a concept that is prevalent in functional programming: monads. We'll learn about JavaScript promises as well, and I'll talk about how we can learn from functional programming when developing in JavaScript.
Monads
If you don't have a functional programming background and don't know what a monad is, I highly recommend this very enlightening post (http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html). Essentially a monad is a design pattern that involves the following:
* A wrapper for a given type -- let's call this `Monad<T>`.
* A function -- let's call it `bind` -- that takes a monad and a callback, and returns a monad.
* The callback passed into `bind` takes a value and returns a monad.
If we were to define a monad in TypeScript, we would see something like this:
class Monad<T> {
value: T;
constructor(value: T) {
this.value = value;
}
}
function bind<T, U>(input: Monad<T>, functor: (value: T) -> Monad<U>) : Monad<U> {
// some implementation here that takes our input and eventually calls functor() with the unwrapped value
// ...
}
So what's the value of monads in the real world? Isn't this just some convoluted way to perform some data transformation? Why can't we just pass a value into a function and get a return value? The answer is that it's not always that simple.
Suppose we want to read a value from a file, perform a transformation on it, and write that to another file. In the world of functional programming, this is a big issue. Functional programming forbids side effects and non-deterministic functions -- that is, we can't mutate any state anywhere whatsoever, and we need to be sure that given a set of parameters, every function will provide the exact same result every time.
Which brings us to our issues: file reading is non-deterministic. The file might not exist, or the contents can change between reads. And file writing, by definition, causes side effects. We modify the contents of a file, thus mutating the state of the file system. What we need to do is protect ourselves from those nasty, scary, stateful operations and deal only with immutable data and deterministic functions.
Functional languages, including Haskell (https://www.haskell.org/), solves this by wrapping these operations within a monad. Essentially, we trust that the runtime will do the scary stuff behind the scenes so that we can write functional code. In the case of file reads, our file reader monad will wrap our file contents, possibly a string. We can then call `bind` on that monad, and pass in a callback that takes a string. In the body of the callback, we would define some functionality (in a deterministic manner!) and have it return some other monad. And then we trust that our function will be executed in due time, after the file I/O happens.
The beauty of this pattern is that we can compose `bind` calls together. Since `bind` itself returns a monad, we can take the result of `bind`, pass it into another call to `bind` with some other callback, and so on. Like this:
function saveCopyrightedStuff(str) {
return someFileWriterMonad('writePath', str + '© ACME, inc.');
}
function done(success) {
if (success) {
return someOutputLoggerMonad('done!');
} else {
return someOutputLoggerMonad('uh oh!');
}
}
bind(bind(someFileReaderMonad('readPath'), saveCopyrightedStuff), done);
Haskell has some very elegant syntax when dealing with monads:
someFileReaderMonad 'readPath' >>= saveCopyrightedStuff >>= done
`>>=` represents our `bind` function here, and without getting too technical or focused on Haskell, whatever is on the left of `>>=` is the monad, and whatever is on the right of `>>=` is the callback.
As you can see, our functions themselves don't do much other than rely on monads to do the dirty work. This is the key takeaway: the functionality we've defined is nice, clean, and functional. We will always return a FileWriterMonad in the first function, and we will always return an OutputLoggerMonad in the second function. We know that our functions themselves don't have side effects -- that's managed down-the-line with our monads. All we do is construct these monads and return them.
When working within a monad, there's no way for the client code to unwrap it. This is the job of `bind`. This makes sense, because we could have some asynchronous task in `bind` that only calls the callback after it's gotten the value, and therefore the value may not be available immediately. Once a monad, always a monad, and you define all of your functionality with `bind`.
Now let's take a break from monads and talk about promises.
Promises
If you're a JavaScript programmer in 2017, you will inevitably come across promises. A promise is an object that wraps an eventual value. A promise has a method called `then`, which takes a callback that takes the eventual value and returns another promise. `then` also returns a promise, so you can chain your callbacks:
function add5(value) {
return Promise.resolve(value + 5);
}
function multiply10(value) {
return Promise.resolve(value * 10);
}
function logIt(value) {
return Promise.resolve(console.log(value));
}
Promise.resolve(3).then(add5).then(multiply10).then(logIt); // logs 80
Promises are useful if you want to compose bits of functionality together, in order, that operate on asynchronous values. The example above doesn't demonstrate this -- you could simply write the expression `(3 + 5) * 10`. But if you need to get the initial value from a file, for instance, you could write something like this:
function getSomeNumberFromFile(fileName) {
return new Promise((resolve, reject) => {
// This callback is called when the file has been read, or there's an error.
function callback(err, data) {
if (err) {
// this lets the promise know that it's been rejected.
reject(err);
} else {
// this lets the promise know that it's been fulfilled.
resolve(data);
}
}
// this reads our file, which happens asynchronously.
fs.readFile(fileName, callback);
});
}
getSomeNumberFromFile('myNumberFile').then(add5).then(multiply10).then(logIt); // logs our result
Promises also cannot be unwrapped, because the value of the promise may not be available immediately. Once a promise, always a promise, and you define all of your functionality with `then`.
The Big Reveal
If you haven't caught on already, what I've been hinting at is that a promise is a type of monad. `then` is the equivalent of the monadic `bind`.
* `then` operates on a monad, which happens to be the promise itself, or `this`. * `then` takes a callback that returns a promise. * `then` returns a promise, upon which `then` can be called as well.
Caveats
There are caveats to the monadic nature of JavaScript promises. The callback of `then` doesn't _have_ to be a promise, and within a callback you can definitely modify some data that lie outside of the promise chain:
let myValue = null;
getSomeNumberFromFile('myNumberFile').then(myNumber => {
myValue = myNumber;
return myValue;
});
This is valid JavaScript, but it's not very functional. The callback itself creates a side effect, and it mutates `myValue`. And behind the scenes, if the return value is not a promise, then it gets wrapped in one. Unlike Haskell, JavaScript is weakly-typed. So a function is under no requirement to return any specific type. You can also choose to return nothing at all. In Haskell, you can't break the type system -- a monad's callback must return a monad.
What now?
Now that we've determined that promises are monads, what does that mean for us, practically speaking?
There's a reason why functional programming is so highly regarded by those who use it. The lack of side effects means you don't have to keep track of too much state. I've been both the victim and, shamefully, the perpetrator of code that involves far too much state. It gets out of control very easily. And in codebases that involve a lot of asynchronous behavior, relying on mutable state that exists outside of the promises that use it can get very tricky very fast.
Another issue that I've run into is the fact that promise callbacks don't need to return a promise. At first glance, it's hard to tell if a function is meant to be used as a promise callback or if it's meant to be used more generally. With a strongly-typed language it's a lot easier, but JavaScript isn't strongly-typed.
So with those two concerns in mind, I think a good case can be made for the following: Don't rely too much on external state when working with promises. A lot can be learned from the functional world, and in general we should rely on passing state as parameters instead of reading and modifying external state. Not only does this make code easier to read and work with, it also makes code more reusable. Functions that don't rely on external state are easier to expose to other parts of the app, because we aren't tightly coupled to some variable in the context in which the function is declared.* This will also ensure that values passed through a promise chain are always resolved or rejected before you use them. No need to check the `null`-ness or `undefined`-ness of a variable, just use it when it's available.Make your `then` callbacks unambiguous. If you know that your function needs to return a promise, make sure that a promise is returned in every code path**. And conversely, if you know that your callback needs to return an unwrapped value, make sure to avoid returning promises in every code path. There are few things more frustrating than attempting to call `then` on a function that doesn't always return a promise. I highly recommend TypeScript (https://www.typescriptlang.org/) to keep your types better managed, but if you have to stick with JavaScript, I recommend documenting your code with JSDoc (http://usejsdoc.org/). JSDoc is supported in many modern JavaScript IDE's.
Conclusion
In the absence of a strong type system and a stateful paradigm, JavaScript developers need to be extra careful when managing state so that their code doesn't become a tangled mess. Dealing with ansynchronicity can get especially messy. We can learn a lot from the way functional languages deal with monads, and we can -- and should -- adopt functional practices when dealing with promises.
* Relying on closure variables isn't always a bad thing and can be very useful when you need to emulate private members. See Crockford's private member pattern in JavaScript. (http://javascript.crockford.com/private.html)
** ECMAScript 2016 introduced `async/await` as an excellent way to manage promises. Not only does it provide syntactical sugar around extracting promise values, it also ensures that every `async` function returns a promise.
Comments