Thinking functionally in JavaScript (part two)

Last time I took a quick look at why JavaScript can be used in a functional manner, primarily though the use of higher-order functions. Another way of putting this is that functions are objects in JavaScript, in the sense that they can be passed to and returned from other functions. And once you say “objects” as a programmer, you start thinking about things like composition, state, inheritance, and so on.

Enter the well-known SOLID principles for object-oriented programming, but how are they applicable (if at all) to functional programming?

The S in SOLID is the simplest principle to remember, at least for me. It’s the Single Responsibility Principle, or to define it in Robert C Martin’s words: a class or module should have one, and only one, reason to change. For functional JavaScript, we can perhaps interpret this to something like: a function should only do one thing and do it well. If we embrace immutability as much as we can (in other words, calling the function does not alter any global state that can be noticed by other functions), then this principle is pretty well always obeyed since a function can only return one output for its inputs. If you think back to my memoize example last time, although the function itself maintained some state, that state was totally contained within the function itself. Calling the result of memoizing some other function would always return the same output.

For functional programming we can perhaps go a step further. Not only should functions do one thing, but we should be prepared to accept that small functions are still worth writing. After all, we don’t know where JavaScript interpreters/compilers are going: it could be that the compilers will inline small functions (maybe they do already?). So we shouldn’t assume the mindset that small functions take too long to call, we should just embrace them. There are other benefits too: simple functions have names, and those names help in documenting the code without recourse to comments; small functions are easy to test with unit tests (and indeed can be seen to be correct as we write them in the first place).

Here’s an example. I was writing some example code a while back for Flot.js. Flot is an open source charting library for JavaScript that works pretty well for displaying charts on your website – and it is reasonably well and fairly regularly maintained. However, the one thing I find incredibly wacky and hard to read is that the data parameter you pass to the Flot function is an array of series, with each series being (at its most basic) an array of points, and each point being an array of two values, x and y. So if you were to write it out literally you’d have an array of arrays of arrays of values.

A couple of Flot’s examples have this bit of code to create a standard chart of y = sin(x). Let’s see how we can improve this by thinking functionally.

var d1 = [];
for (var i = 0; i < 14; i += 0.5) {
  d1.push([i, Math.sin(i)]);
}

$.plot("#placeholder", [d1]);

(The plot() function is Flot’s method of displaying a chart, and as you may surmise it’s a jQuery plug-in.) As far as it goes, fairly understandable code, although to me some of the naming is highly suspect. The first thing I would do is wrap the code that creates the data series into a function and rename the identifiers:

var makeSineSeries = function () {
  var x;
  var series = [];
  for (x = 0; x < 14; x += 0.5) {
    series.push([x, Math.sin(x)]);
  }
  return series;
};

$.plot("#placeholder", [makeSineSeries()]);

Now consider that new function makeSineSeries(). It is generating a series of points to plot, sure, but imagine that our next job is to create a similar chart for cosine. Do we just copy-and-paste that function and change the call to sin() to a call to cos()? Of course not. The name of the function even gives a hint: it has two reasons for change: there’s the generation of the data series (we’ll get back to that in a moment) and there’s the function that gets called to calculate the y values (currently the sine function). Why hard-code that? Why not pass that function in?

var makeSeries = function (fn) {
  var x;
  var series = [];
  for (x = 0; x < 14; x += 0.5) {
    series.push([x, fn(x)]);
  }
  return series;
};

$.plot("#placeholder", [makeSeries(Math.sin), makeSeries(Math.cos)]);

As you can see, I’ve now got Flot displaying two separate series, one for sine and one for cosine, using this more “generic” makeSeries() function. There’s another opportunity here: we’ve got some values hardcoded (the starting x value, the ending x value, and the increment), so let’s make those parameters too.

var makeSeries = function (config, fn) {
  var makePoint = function (x, fn) {
    return [x, fn(x)];
  };
  var x;
  var series = [];
  for (x = config.start; x < config.end; x += config.increment) {
    series.push(makePoint(x, fn));
  }
  return series;
};

var seriesConfig = {
  start: 0,
  end: 14,
  increment: 0.25
};

$.plot("#placeholder", [
  makeSeries(seriesConfig, Math.sin),
  makeSeries(seriesConfig, Math.cos)
]);

I decided to make a configuration object for these values: I dislike too many parameters, and with a configuration object the values are named, and I can reuse it. I’ve also created this makePoint() function to isolate the creation of a 2D point for the chart, and also (ahem) to make a point. Notice that the function that is passed in is constant to that makePoint() function in a single call to makeSeries(). We should be able to wrap that so that we don’t have to pass it in all the time.

var makeSeries = function (config, makePoint) {
  var x;
  var series = [];
  for (x = config.start; x < config.end; x += config.increment) {
    series.push(makePoint(x));
  }
  return series;
};

var pointMaker = function (fn) {
  return function (x) {
    return [x, fn(x)];
  }
};

var seriesConfig = {
  start: 0,
  end: 14,
  increment: 0.25
};

$.plot("#placeholder", [
  makeSeries(seriesConfig, pointMaker(Math.sin)),
  makeSeries(seriesConfig, pointMaker(Math.cos))
]);

Here, the pointMaker() function is a function that returns another function. This returned function takes an x value and returns a 2D point, based on the parameter function passed into the pointMaker() function.

At this point, the only thing bugging me in a functional sense is the iteration inside makeSeries(). It should be replaced with some kind of recursion, preferably tail-end recursion, to be properly functional, but since all modern browsers at the time of writing do not yet support tail-call optimization, there is a distinct possibility of blowing the stack. One day perhaps, I’ll revisit this.

(Aside: here’s a recursive example:

var makeSeries = function (config, makePoint) {
  var addPoint = function (arr, point) {
    return arr.concat([point]);;
  };

  var recurseMakeSeries = function (arr, x) {
    if (x > config.end) { return arr; }
    return recurseMakeSeries(
      addPoint(arr, makePoint(x)),
      x + config.increment
    );
  };

  return recurseMakeSeries([], config.start);
};

It works, it’s tail-end, but I’m not very pleased with it.)

So, at the end of this small exercise, we have two functions: pointMaker() that returns a function to output a single 2D point for a chart based on a function we supply, and makeSeries() that creates an array of 2D points for a chart once passed some configuration and a function that given an x value outputs a 2D point. Both are “single responsibility” functions in that the only reason for them to change would be (1) we change the way 2D points are created, or (2) we change what is present in a data series.

Here’s the final version as a webpage, with the basic structure copied from this Flot demo page.

Next time, we’ll continue as far as we can with our exploration of SOLID principles in functional JavaScript.

Kissing gate

Now playing:
Springfield, Dusty - Goin' Back
(from The Very Best of Dusty Springfield [Polygram])


Loading similar posts...   Loading links to posts on similar topics...

No Responses

Feel free to add a comment...

Leave a response

Note: some MarkDown is allowed, but HTML is not. Expand to show what's available.

  •  Emphasize with italics: surround word with underscores _emphasis_
  •  Emphasize strongly: surround word with double-asterisks **strong**
  •  Link: surround text with square brackets, url with parentheses [text](url)
  •  Inline code: surround text with backticks `IEnumerable`
  •  Unordered list: start each line with an asterisk, space * an item
  •  Ordered list: start each line with a digit, period, space 1. an item
  •  Insert code block: start each line with four spaces
  •  Insert blockquote: start each line with right-angle-bracket, space > Now is the time...
Preview of response