Let's Make a Framework: Functional Programming Part 2

25 Mar 2010 | By Alex Young | Tags web frameworks tutorials lmaf functional

Welcome to part 5 of Let’s Make a Framework, the ongoing series about building a JavaScript framework. This part continues the work last week on functional programming.

If you haven’t been following along, these articles are tagged with lmaf. The project we’re creating is called Turing and is available on GitHub: turing.js. You can contribute! Fork and message me with your changes.

More Functional Methods

Last week I intimated that each formed the basis of our functional programming library. This week I’ll show you how to add more methods that build on each. I’ll draw on inspiration from Underscore and Prototype, not to mention JavaScript’s more recent Array.prototype methods.

Filter

Filter allows you to remove values from a list:

turing.enumerable.filter([1, 2, 3, 4, 5, 6], function(n) { return n % 2 == 0; });
// 2,4,6

That means the implementation needs to:

  1. Check if there’s a native filter method and use it if possible
  2. Else use turing.enumerable.each
  3. Filter objects into multi-dimensional arrays if required

The tests need to check that both arrays and objects are handled. We already used this approach last week:

Riot.context('turing.enumerable.js', function() {
  given('an array', function() {
    var a = [1, 2, 3, 4, 5];

    should('filter arrays', function() {
      return turing.enumerable.filter(a, function(n) { return n % 2 == 0; });
    }).equals([2, 4]);
  });

  given('an object', function() {
    var obj = { one: '1', two: '2', three: '3' };

    should('filter objects and return a multi-dimensional array', function() {
      return turing.enumerable.filter(obj, function(v, i) { return v < 2; })[0][0];
    }).equals('one');
  });
});

I’ve tried to be sensible about handling both objects and arrays. Underscore supports filtering objects, but returns a slightly different result (it just returns the value instead of key/value).

Detect

Detect is slightly different to filter because there isn’t an ECMAScript method. It’s easy to use though:

turing.enumerable.detect(['bob', 'sam', 'bill'], function(name) { return name === 'bob'; });
// bob

This class of methods is interesting because it requires an early break. You may have noticed that the each method had some exception handling that checked for Break:

each: function(enumerable, callback, context) {
  try {
    // The very soul of each
  } catch(e) {
    if (e != turing.enumerable.Break) throw e;
  }

  return enumerable;
}

Detect simply uses each with the user-supplied callback, until a truthy value is returned. Then it throws a Break.

Chaining

We need to be able to chain these calls if we can honestly say turing.js is useful. Chaining is natural when you’ve overridden Array.prototype like some libraries do, but seeing as we’re being good namespacers we need to create an API for it.

I’d like it to look like this (which is different to Underscore):

turing.enumerable.chain([1, 2, 3, 4]).filter(function(n) { return n % 2 == 0; }).map(function(n) { return n * 10; }).values();

Chained functions are possible when each function returns an object that can be used to call the next one. If this looks confusing to you, it might help to break it down:

.chain([1, 2, 3, 4])                         // Start a new "chain" using an array
.filter(function(n) { return n % 2 == 0; })  // Filter out odd numbers
.map(function(n) { return n * 10; })         // Multiply each number by 10
.values();                                   // Fetch the values

To make this possible we need a class with the following features:

  • Store temporary values
  • Runs appropriate methods from turing.enumerable by mapping the temporary value into the first argument
  • After running the method, return this so the chain can continue

This is all easily possible using closures and apply:

// store temporary values in this.results
turing.enumerable.Chainer = turing.Class({
  initialize: function(values) {
    this.results = values;
  },

  values: function() {
    return this.results;
  }
});

// Map selected methods by wrapping them in a closure that returns this each time
turing.enumerable.each(['map', 'detect', 'filter'], function(methodName) {
  var method = turing.enumerable[methodName];
  turing.enumerable.Chainer.prototype[methodName] = function() {
    var args = Array.prototype.slice.call(arguments);
    args.unshift(this.results);
    this.results = method.apply(this, args);
    return this;
  }
});

Conclusion

Now you know how to:

  • Check for native methods that operate on collections of values
  • Implement them using each where required
  • Break early using an exception
  • Chain methods using closures
  • All in a safely namespaced API!

If you’d like to implement more enumerable methods, check out the ones that Underscore supports and port them to turing’s style and naming conventions. Add a test or two, let me know via GitHub, and I’ll include your contribution and add you to the contributor list.


blog comments powered by Disqus