Skip to content

Instantly share code, notes, and snippets.

@dillonforrest
Created August 19, 2013 00:38
Show Gist options
  • Save dillonforrest/6264883 to your computer and use it in GitHub Desktop.
Save dillonforrest/6264883 to your computer and use it in GitHub Desktop.
Applying functional programming techniques to some common Node.js callback patterns

Functional programming exercises

This is a thought exercise. Nothing to be taken seriously. I'm not a serious node programmer (yet), and I'm still relatively new to functional programming. But, I'd like to look at some common node patterns using callbacks and apply funtional programming techniques to them. Some of these exercises might seem impractical or contrived, but the point is learning. Seriously, I'm very new to this. Please be forgiving if I make a stupid mistake, but also let me know. :) Let's have some fun!

The examples are derived from Andrew Kelley's article here.

The simplest example

This is directly from Andrew's post:

function getUserFriends(userName, next) {
    db.users.findOne({name:userName}, function (err, user) {
        if (err != null) return next(err);
        db.friends.find({userId:user.id}, function (err, friends) {
            if (err != null) return next(err);
            return next(null, friends);
        });
    });
}

Andrew refactors it to this:

function getUserFriends(userName, next) {
    db.users.findOne({name:userName}, foundOne);

    function foundOne(err, user) {
        if (err != null) return next(err);
        db.friends.find({userId:user.id}, foundFriends);
    }

    function foundFriends(err, friends) {
        if (err != null) return next(err);
        return next(null, friends);
    }
}

From what I've seen about node at this point, if (err) return next(err); is a pretty common pattern. Let's abstract that.

function catchErr(next, success) {
  return function(err, result) {
    if (err != null) return next(err);
    return success(friends);
  };
}

I've created a function factory, catchErr to handle error-catching logic. Now, we can further refactor the above to the following:

var _ = require('underscore');

function getUserFriends(userName, next) {
  var returnResults = _.partial(next, null);
  var findFriends = catchErr(next, function(user) {
    return db.friends.find({ userId: user.id }, returnResults);
  });
  var findUser = catchErr(next, function(userName) {
    return db.users.find({ name: userName }, findFriends);
  });
  
  findUser();
}

Now, the code is a bit drier. I'm no longer repeating if (err != null) return next(err);. Cool!

I'd also like to address testability and extensibility. The db.friends.find() and db.users.find() methods should ideally be tested and flexible enough to be used with any callback. Let's abstract that:

function findFriendsThen(callback) {
  return function(user) {
    return db.friends.find({ userId: user.id }, callback);
  };
}

function findUserThen(callback) {
  return function(userName) {
    return db.users.find({ name: userName }, callback);
  };
};

I've created more function factories. Here, we can test both functions. They're no longer trapped as private, untestable methods inside the getUserFriends parent function. Additionally, we can create any number of instances to first either find users by user name or find friends by user id, then execute any given callback. Improved units and unit tests means more meaningful integration tests down the line, and real business value is ensured with integration tests.

Our getUserFriends function becomes the following:

function getUserFriends(userName) {
  var returnResults = _.partial(next, null);
  var findFriends = catchErr(next, findFriendsThen(returnResults));
  var findUser = catchErr(next, findUserThen(findFriends));
  
  findUser();
}

It's more concise now. Our getUserFriends implementation is reduced to just the core logic necessary to know how exactly it will work. This is one of the awesome things about functional programming. You can abstract away the bits which aren't actually relevant to the function at hand. From what we have here, you can see that getUserFriends first finds a user, then finds friends, then returns results. That's all we needed to know, and functioanl programming allows us to hide all the plumbing to make it work.

Now, at this point, I should definitely clarify that you can refactor indefinitely. At this point, I'd be okay with leaving it as is.

Conclusion

While this was a really good exercise for me, there's a very meaningful takeaway. Just use promises. Callbacks can sometimes be okay, but most of the time, they're probably not human-readable. Though I believe my refactors made the code cleaner and more readable, I still believe having to read bottom-up rather than top-down is unnecessary and suggests a different approach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment