More Functional Javascript: Reducing Promises, Ramda.js, & Arrow functions again

In a previous article, I introduced a neat little pattern I use to leverage Promise.then() so that I can compose both synchronous and asynchronous functions together into a sort of sequential pipeline. You find similar patterns in things like node’s run sequence and Ramda.js pipeP.

The basic outline of my version was this:

..and the result was that you could do things like this:

(“Program” was probably a silly, way too general name for the end result, bwhatevr. That way of describing what I was up to was helpful in the context of the previous article.)

Now while I happen to think what’s going on here is really elegant, that original code up there, using vanilla lodash, doesn’t necessarily express that elegance all that well.

So instead, let’s explore how we can express the core idea in an even more functional way with libraries like Ramda.js (lodash-fp is also an option, but let’s stick to Ramda for now) and/or ES2015 syntax.


First, let’s recap what we’re up to here in a bit more detail. We have a list of simple, single-argument, single-return functions, some of which might return Promises & some of which might just return new, modified values. Promise.then(fn) works with either sort. We want to take that list (Array) of functions and end up with a single Promise that represents all of those functions composed to run in a sequence. How would we go about doing that?

Well before we answer that question, let’s ask a better question. That description of what we’re trying to do was way way too specific! So let’s start over:

We want to take a list of things and end up with a single value.

So: what fundamental operation does something like that?

Reduce. Reduce is a standard, well-defined higher-order operation that takes a type (in this case a list of things like an Array) and then folds all of its (possible) inner values down into a new value. Exactly what happens on each step is defined by a function often called the “iterator.”

There are many, many possible ways to “reduce” a list of things down into a single value. And that’s because there are many, many possible iterator functions. So when thinking about which sort of reduce operation we want, that core function is the first and most fundamental thing. Unlike the implementation of _.reduce in functional libraries like underscore, Ramda’s version of reduce very conveniently (and very intentionally) takes that function as its first argument. Here’s the simplest version of “reduce” I can think of with an iterator already baked in:

R.reduce( (acc,x) => acc+x )

Because all of Ramda’s methods are “curried,” writing and executing that code by itself doesn’t return an error or a value: it will instead return a new function: a particular reduction operation that could be used over and over on different lists or different starting points. In this case, the function will simply “add up” any Array of things. Pass it a list of numbers and a 0, and it’ll sum. Pass it a list of strings and an empty string, and it’ll join. And so on.


The specific type of reduce operation we ultimately want with Promise chains is really very simple: given some core starting value (which will always be a Promise) we just wanted to call its method .then() using the next function in our sequence as the argument. This will return a new Promise. That new Promise, representing the entire chained composition so far, will then be the new starting point for the next loop of the iterator (because it, being a Promise, is guaranteed to have a .then() method).

Voila:

Note again that we haven’t yet specified which list, or what the starting point will be (though we know implicitly that the starting point will be a Promise and the list will contain functions). We’ve instead just expressed a concept that stands on its own, without drowning ourselves in function keywords, line breaks, or even, thanks to Ramda, any explicit currying.

The ES2105 Arrow function syntax is especially nice to have here as well, because it allows us to clearly explain that one core operation as a sort of formula… without all the boilerplate. Given any “p” that happens to have a .then() method, give us back the result of applying each function in a list to p using .then(). That’s all we need to say, and with ES2015, that’s all we have to say.

The end result, in just a few characters, is simple, expressive, and already does 90% of what we want!

pipePromise($.ajax(…),[fn1,fn2,fn3,fn4]);


Note that the native javascript Array method, Array.prototype.reduce, doesn’t really let us work quite this way. There, we generally have to have a specific Array already picked out to even have access to a .reduce() method on it. And we’re forced to specify the accumulator ahead of time too. Arg!

To get around that restriction without any special functional library we’d instead have to create a function that takes the iterator function and then explicitly (without auto-currying) returns a new (also non-curried) function waiting for the list and the accumulator. Here’s that simple “adding up” version of reduce created by those two steps:

That’s still a lot to take in though. With the new Arrow function syntax in ES2015 we can actually describe that same thing in a much cleaner, more direct way:

This is exactly the same code as before, but now it’s almost like an equation, without all the {}’s and “function” and “return.” In English, we’re saying this: “return a function that takes one argument (an iterator function) and then returns a function that takes two arguments, an Array and an accumulator, which then will return the result of running reduce on the Array, using the iterator function and the accumulator as a starting point.”

Arrow functions allow us to define that complex closure-driven pattern (taking an argument, returning a new function that accepts more arguments, then finally returning a result that uses all the enclosed arguments) in a really neat way that’s a lot more readable.

If you’re familiar with Haskell’s type annotations (Int -> Int -> Int -> String), this certainly isn’t it, but there’s a similar sort of look/logic: each new argument (or, here, grouping of arguments) returns a new function that takes in one more argument, and again and again until a final value is returned. There’s no inherent, syntactical distinction being made between arguments vs a final result. It just happens to be final because, well, it’s last, not because it’s special.


Note though that, unlike with Ramda and its curried “reduce” or Haskell which treats each step as a accepting a single argument, when we define functions this Arrow function way we still have to explicitly define the groupings of arguments and what they’ll return at each step. And that means that when it comes time to use the entire construct, we can only pass the arguments in exactly as we originally grouped them. If we wanted instead to pass only one argument at a time, we’d just have to define/do/group things a bit differently:

This is all still pretty powerful and expressive, it just isn’t quite as flexible as it might be with auto-currying in place.


Oh, and we said “90% of what we wanted” before, right? Well, we need to talk about the last 10%, which is just…

Now, maybe that looks a bit complex, but it’s really just a touch of syntax manipulation so that we can specify the rest of the arguments in a convenient way.

First, note that our original promise-y reducing function, pipePromise, took a starting Promise first, and then expects the list of functions as a single Array (that’s simply how Ramda chose to order them, and their way, while inconvenient here, makes more sense in most cases). We’d rather it take the list of functions first… and ideally let us express that list as variadic arguments to a single function.

That is, instead of passing a single array of functions as an argument, we’d want to just be able to list out all the functions as arguments to our new function. That’s not really super important (and may not even be a good idea in your use case!) but it does mean that we’ll be able to use program like program(fn,fn,fn) instead of having to use it like program([fn,fn,fn]) (though both ways will still work, as we’ll see).

How did we achieve that new syntax/behavior? By simply defining what operations get us there of course! Once we know what they are, our old functional friend compose will allow us to chain those operations together, and Ramda’s auto-currying will allow us to pre-configure any of those chained operations as needed. In the end, we get a simple pathway that a single argument can just flow on through to the correct result.


Let’s walk through program bit by bit, starting with R.unapply. this higher-order function allows us to treat some other function as if it took a list of arguments instead of taking a single array. That’s exactly what we wanted. Which function do we want to apply that transformation to? R.flatten, which will recursively unnest any Arrays of arrays: another behavior that we wanted.

Ok, so all of that R.unapply(R.flatten) stuff was just to allow us to transform (fn1,fn2,fn3,[fn4,fn5],fn6) into [fn1,fn2,fn3,fn4,fn5,fn6]: the single Array format that pipePromise expects. But wait: that’s still wrong. Our pipePromise function expects the array of functions LAST. That is, the way its arguments are ordered, it “wants” the accumulator (the initial Promise) passed in first… but we agreed that the whole point here was to create a pre-defined “program” of actions we could later use on any starting value we please, over and over.

Well, that’s easy enough: we just switch the order of the two further arguments that a pipePromise expects by wrapping it up in the higher-order function R.flip. The resulting function now takes the list of functions first. And because all Ramda functions are curried, if we only pass it the list, then we’ll still get a function back: one that takes just that single, final argument: the Promise/accumulator.

Now we can call program(fn1,fn2,fn3,[fn4,fn5],fn6) and get back a complete functional pipeline. And the way we built it was both beautiful and declarative!


We can still do better though: the way I’ve built program requires that a program’s final argument be a Promise, which might seem unwieldily at times. I mostly wrote it that way to be explicit and illustrate a point but if that’s annoying, we can actually fix it pretty easily. ES2015 Promises currently have this neat property:

Promise.resolve(Promise.resolve(5)) = Promise.resolve(5)

That means that if we could just wrap the accumulator argument in a Promise.resolve() before it gets used, then regardless of whether we pass in a bare value or a resolved Promise, the result will be the same: a resolved Promise.

So the real only question is how we get it in there. It’s not quite as elegant, but one way might be with the higher-order function R.useWith:

Basically, the accumulator argument here gets wrapped in a transforming function that ensures that it’ll be a Promise, no matter what.

Alternatively, we could just more explicitly define the underlying reduce function itself such that it always wraps the accumulator in a Promise right out of the box:

We’d just have to remember to re-curryify the resulting function so that it works the same. And heck, if we’re going to refactor “reduce” a bit anyhow, why not just reorder the accumulator/list arguments, avoiding the need to to the R.flip transformation later on?

Now, instead of having to explicitly starting programs off with values wrapped in Promises, we can also just pass them values if that’s how we want to structure things:

Pachinko.


Can we do something similarly cool just using ES2015? Oh yeah:

Using ES2015's rest parameters (e.g., …list), we’ve basically gotten rid of the need for something like R.unapply and thus even the need for R.compose as well.

The only thing we’re really missing now is the flatten operation (which isn’t at all necessary, but sometimes it’s nice to group things in complex compositions), which we’d have to either implement ourselves or go back to a functional library for a leg up:

And with just that, we’ve now spelled out both the original core functionality we wanted and exactly how, step by step, the arguments to program will be specified. This wasn’t just code golf for the sake of it either: this is powerful stuff, expressed in a really clean, declarative way.