Side Effects vs. Promises

Oct 2, 2015 01:47 · 727 words · 4 minute read

This past weekend, I saw Chris Armstrong (@radix) talk about Side Effects as Public API. From the description on the Strange Loop page for the talk:

Haskellers have been isolating their side-effects from their pure code for decades, but most people write code in languages that allow ad hoc side-effects anywhere. In a lot of cases, these side-effects are practically a part of the public API — they put restrictions the way the code can be used, often obscurely, and have effects that are observable to the caller. It helps a lot to acknowledge this and explicitly treat them as a part of your public API. Here are the basic ideas that I’ll cover:

  • represent your side-effects as “intents to perform an action” — transparent objects that expose the details of the side-effect, without actually performing it
  • combine effects with pure code to describe the execution order and data dependencies (yes, like a monad)
  • write unit tests for your code without mocking, by specifying the expected content, results, and order of side-effects performed by a function

Chris has made a Python library called Effect that implements the ideas he was presenting.

In JavaScript-land, we’ve embraced the idea of promises now to the point that they’re a standard part of the language, already implemented in multiple browsers (about 66% of those in use!) and in Node. Promises can clean up your asynchronous code and if you’re not familiar with them, you should go read one of the many tutorials about them.

A promise is a placeholder for a value to come along at some point in the future. Using the then method on the Promise, you can pass in a function to be called when the value is ready. Here’s a hypothetical example:

function getItemOne() {
    let promise = get("http://foo.barbaz/api/item/1").then((value) => {
        // Do something with the result
    }, (error) => {
        // Do something with the error
    });
    return promise;
}

This example above is not very different from the use of callbacks and isn’t intended to show off the nice attributes of promises. It is a good example to illustrate the use of an Effect rather than a Promise.

The first thing I’ll note is that getItemOne is not a pure function. Calling it produces a side effect: an HTTP request. Testing this function, or any function that calls it, is difficult because of that impurity. The usual solution for testing something like this is to replace get with a mock function. That doesn’t really fix the impurity of the function, but it at least makes it testable.

We can make getItemOne pure by making a small change like this:

function getItemOne() {
    return new SideEffect({
        type: "http-get",
        url: "http://foo.barbaz/api/item/1"
    }).on((value) => {
        // Do something with the result
    }, (error) => {
        // Do something with the error
    });
}

I’m not suggesting that we’d use the API above, because I think we can do better than that. In fact, this version could look the same as the previous version, if we wanted it to. But, I wanted to clearly show the difference between promises and the side effects about which I’m writing.

The function above is pure. Every time you call it, it will return exactly the same value. Calling this from a test is easy and you can verify that the side effect returned matches your expectations. You can also pass in values to test what happens in the success or failure cases, which would also be pure functions.

In a real system, somewhere up the call chain there will be a dispatcher called to actually process the side effect. The Python Effect library points out that this approach allows you to easily swap out how any given side effect is processed.

I asked Chris if he knew of any JS implementations of Effect. He wasn’t aware of any, but he pointed out that it’s basically just the IO monad. Here’s an implementation of the IO monad in JS. If you look at the Python Effect library and its examples, though, I think there’s a need for a JS library that fits in better with the overall JS ecosystem (test runners and whatnot).

In practice, returning side effects rather than performing them and returning promises can increase the size of the “functional core” of your application, which is a win in my book.