How do you compose JavaScript functions with multiple parameters?
As functional programmers, we like to piece our programs together out of small pieces. Our main tool for this is composition. We take an input, process it through a function, then pass it on to another function. And this is great, so long as all our functions take exactly one argument. Which never happens. So, what do we do? In general, we turn to a set of tools called function combinators.
Now, combinators are a big topic and can get quite abstract and confusing. So we’re going to focus on one specific problem that comes up a lot. And we’ll show how one special combinator solves that problem for us.
A sample problem
So, what’s the problem? Well, let’s imagine that you’re processing a bunch of JSON data. Often, we end up needing to extract two bits of data, process them, then piece them back together. For example, imagine we have a person object:
const person = {
firstname: 'Horatio',
lastname: 'Alderaan',
};
And we would like to grab the first name and last name out of that object separately. Then we want to combine the results in a message (yes, yes, it’s a silly example; bear with me). Here are our extraction functions:
function firstname(p) {
return p.firstname;
}
function lastname(p) {
return p.lastname;
}
We’d like to use both those names in a message:
function compliment(first, last) {
return `Why hello, ${first} ${last}. You look most fetching today.`;
}
But of course, because we’re functional programmers, we always curry our functions:
function compliment(first) {
return last => `Why hello, ${first} ${last}. You look most fetching today.`;
}
Now writing this out in an imperative style is easy:
const complimentPerson = person => compliment(firstname(person))(lastname(person));
But in functional programming, we like to compose
our functions. And besides, it’s a pain to write person
twice. What’s more, this pattern comes up all the time. We want something that will take our three functions and pipe them together. And, do it in such a way that all the inputs and outputs line up. Something like the diagram below:
You’d think there’d be a standard combinator for this pattern… and it turns out there is, but it’s hard to find. It took some digging before I found a name for it. It’s called lift. So, how does it work?
- It takes three function inputs. The first takes two parameters (a binary function). The other two take only one parameter (unary functions).
- It returns a function that accepts a single parameter. It passes this parameter to both the unary functions. Then, it uses the binary function to combine the two results.
It’s easier to explain in code:
const lift2 = f => g => h => x => f(g(x))(h(x));
We call it lift2
because this version lets us work with binary functions (that is, functions that take two parameters). We could also write lift3
for functions that take three parameters, and so on.
Now that we’ve created our lift2
function, we can use it like so:
const complimentPerson = lift2(compliment)(firstname)(lastname);
console.log(complimentPerson(person));
// ⦘ Why hello, Horatio Alderaan. You look most fetching today.
A more realistic example
Now, that was a trivial example. We didn’t really need lift2
to solve that one. We could have written a simple template function instead. So let’s try another example (inspired by this comment). We’ll try something that deals with ‘real world’ stuff, like the DOM. Imagine we have a form specification that looks something like this:
const formSpec = [
{
type: 'text',
label: 'Name',
id: 'name',
},
{
type: 'email',
label: 'Email',
id: 'email-address',
},
];
We want to create a set of HTML form fields based on this specification. We create a function for the form label, and another for the input. (We’ll ignore impurities for the moment, because we’re focusing on composition).
function createLabel(doc) {
return obj => {
const lbl = doc.createElement('label');
lbl.setAttribute('for', obj.id);
lbl.innerHTML = obj.label;
return lbl;
};
}
function createInput(doc) {
return obj => {
const inpt = doc.createElement('input');
inpt.setAttribute('type', obj.type);
inpt.setAttribute('id', obj.id);
return inpt;
};
}
It would be nice if we could map over our specification and create a bunch of <div>
s. With each <div>
containing a label–input pair. We’ll create a function that makes a new <div>
element, then appends our label and input:
function inputAndLabel(doc) {
return inpt => lbl => {
const div = doc.createElement('div');
div.appendChild(lbl);
div.appendChild(inpt);
return div;
}
}
With lift2
, we can glue everything together nicely:
const map = f => a => a.map(f);
const makeControl = inputAndLabel(window.document);
const label = createLabel(window.document);
const input = createInput(window.document);
const processFormItem = lift2(makeControl)(label)(input);
const formItems = map(processFormItem)(formSpec);
It might not seem like much, but I find myself wanting something like this all the time. You may be wondering though, what if we needed to converge three functions? Or four? Or five? The Ramda library has you covered. Ramda includes a function called converge()
. It lets you specify as many functions as you like in an array. So you would use it like this:
import {converge} from 'ramda';
const complimentPerson = converge(compliment, [firstname, lastname]);
const processFormItem = converge(makeControl, [label, input]);
So, why not give it a go? Next time you find yourself needing some data passed to two functions, try lift. And if you do find it handy, please send me a tweet and let me know.