Blog

Using Lambdas with Bluebird.Map

A situation came up this week where a coworker had stumbled upon some really terse code. They weren’t exactly clear on what was going on, so we went through an exercise together of rewriting the code in the most verbose format, then gradually shrinking it one piece at a time to arrive at the very terse but completely equivalent format.

Our Task: Put Users into Groups

Here’s the situation. We need to do a series of database inserts to add a user to multiple groups. This is done with an SQL INSERT statement for each groupId linking that group to the given user.

Here’s the initial setup code that will be unchanged for all the examples in this post:

const Bluebird = require('bluebird')

const db = {
  insert: (table, row, callback) => {
    setTimeout(() => {
      console.log('done adding row', row)
      callback(null, [])
    }, 1000)
  }
}
Bluebird.promisifyAll(db)

function addToGroup(userId, groupId) {
  console.log('adding user', userId, 'to group', groupId)
  return db.insertAsync('users_groups', {user_id: userId, group_id: groupId})
}

WTF is this?

The initial code encountered, which was unclear, was

const groupIds = [42, 27, 33, 99]
const userId = 12

Bluebird.map(groupIds, addToGroup.bind(null, userId))
  .then(() => {
    console.log('done')
  })

This makes use of Function.prototype.bind which is available on all functions. To show what’s going in in this very terse bit of code, we coded up the fully verbose format of equivalent code as follows.

Bluebird.map(groupIds, function perGroup (groupId) {
  return addToGroup(userId, groupId)
})
  .then(() => {
    console.log('done')
  })

We’re using Bluebird.map to iterate over each groupId in the groupIds array so the perGroup function will be invoked 4 times since there are 4 values in the groupIds array. Each invocation calls addToGroup with the arguments in the proper order, and returns the promise that addToGroup returns.

addToGroup does the DB insert against our mocked-up database library which has been promisified with the Bluebird.promisifyAll() utility function so insertAsync will return a bluebird promise and not need a callback even though insert is coded as a callback API.

When coded like that, it was clearer what was going on. So we then started to trim out syntax that is technically unnecessary step by step as a learning exercise.

Shrinking it down

First to go was just the name of the perGroup function, making it a lambda (anonymous function).

Bluebird.map(groupIds, function (groupId) {
  return addToGroup(userId, groupId)
})
  .then(() => {
    console.log('done')
  })

Sprinkle Some ES2015 Arrow Function On It

Next we opted for the terser ES2015 arrow function syntax.

Bluebird.map(groupIds, (groupId) => {
  return addToGroup(userId, groupId)
})
  .then(() => {
    console.log('done')
  })

Lose the Curly Braces

Now what we notice is we have an arrow function that just returns the value of a single expression. This means it’s eligible for the ultra-terse no-curly form. So we axe the curlies and the return keyword and keep it on one line.

Bluebird.map(groupIds, (groupId) => addToGroup(userId, groupId))
  .then(() => {
    console.log('done')
  })

OK now that’s pretty terse. However, look closely at the anonymous mapping function itself (groupId) => addToGroup(userId, groupId). How different is it from addToGroup? Well, instead of taking 2 arguments, it only takes one. Then it calls addToGroup with 2 arguments, the first being the userId which is directly in the parent scope and the second being the groupId that comes from Bluebird.map. So you could think of it like a variant of addToGroup with the userId argument hard coded or “baked in” to be a particular value.

It turns out that this is exactly what Function.prototype.bind does. Given a function, bind creates a new function that has some of the arguments pre-specified (“bound”) to particular values, leaving only the remaining arguments to be varied on each function call.

Back to .bind

So that gets us back to the .bind format.

Bluebird.map(groupIds, addToGroup.bind(null, userId))
  .then(() => {
    console.log('done')
  })

It’s a few characters shorter than even the tersest arrow function.

What we are doing here is creating a new, unnamed function with pre-specified this value and first argument value. When we call addToGroup.bind(null, userId) passing null as the first argument means the function bind returns will not have access to a this variable when called. addToGroup is a regular function (not an object-oriented method) and makes no reference to this, so it’s perfectly OK to have it be null.

The second argument to bind will be “bound” as the first argument of our new addToGroup variant. That leaves the 2nd argument unspecified, which lines up with the Bluebird.map expecting a function that takes 1 argument which is a groupId. Remember that bind returns the new function without executing it. So you could express this in English as “Hey addToGroup function, build me a variant of yourself with this userId fixed as the first argument and the other argument unspecified until later when I call the function”.

lodash partial version

Another option if that ugly null in the .bind call rubs you the wrong way would be lodash’s _.partial which looks like this:

Bluebird.map(groupIds, _.partial(addToGroup, [userId]))
  .then(() => {
    console.log('done')
  })

Lessons Learned

My take is that the implicit-return arrow function flavor: Bluebird.map(groupIds, (groupId) => addToGroup(userId, groupId)) is the tersest you can go and still be readable to developers without much exposure to .bind, which is a fairly advanced feature in regular JS codebases, although in functional programming it implements the idea of partial application, which is very commonplace and not advanced for developers accustomed to the functional programming paradigm.

I think explicitly seeing that you have a function that takes a single groupId argument and gets userId from the parent scope is important for this to be clear, and with .bind you lose that and it becomes magic.

The main takeaway is that all formats of this code are exactly equivalent. It’s good to be able to read and understand every variation so you are prepared if you encounter it in your coding.

Thanks to Scott Nonnenberg for reviewing a draft of this post!

Gorilla Logic is always on the hunt for the best software engineers. Think you have what it takes to be a Gorilla? Check out our career section by clicking below 

Ready to be Unstoppable? Partner with Gorilla Logic, and you can be.

TALK TO OUR SALES TEAM