Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
119 lines (78 sloc) 5.78 KB

The Madness of King JavaScript

From time to time, people notice something is very wrong with functional-style programming in JavaScript. For example:

['1', '2', '3'].map(parseFloat);
  //=> [1, 2, 3]
  
// HOWEVER:

['1', '2', '3'].map(parseInt);
  //=> [ 1, NaN, NaN ]

Shouts of "William-Thomas-Fredrick" ensue. The strange behaviour of .map(parseInt) is caused by the interaction of one language choice and two library choices. When all three come together, you get an unexpected result:

  1. The language feature is that JavaScript does not enforce function arity. So you can define a function with three arguments but pass one parameter. Or pass five parameters to a function that only expects one.
  2. .map is designed such that it passes three parameters to the mapper function. The first is the element, the second is the index of the element, and the third is the context (the array, in this case).
  3. .parseInt works just fine with one parameter, but you can also pass an additional parameter to define the radix.

So what happens here? Well, the .map method is passing in a string and a number on each call, and parseInt is interpreting that number as a radix. Boom.

Okay. So?

Well, reasonable people can take either or both of the following two positions:

  1. You need to know your language and libraries. If you don't, you're at fault.
  2. Libraries should be designed to avoid suprises. .map being so "helpful" with the extra parameters is not helpful and contrary to practice elsewhere. This is a design problem, not a programmer problem. (There should be a .map and a .mapWithIndex in the library, not one function trying to do too many jobs)

There's plenty of opportunity to debate those positions, but if you have chosen to use JavaScript because "On the whole it's a good thing for a particular project," here are two different things you can do to avoid surprising yourself or colleagues who haven't read this blog post.

first, use a safer mapping construct

There's nothing wrong with rolling your own mapper. I'm a big fan of combinators, so I use .splat from the allong.es library. Splat looks something like this:

function splat (fn) {
  fn = functionalize(fn);
  return function (list) {
    return __map.call(list, function (something) { return fn(something) });
  };
};

And we use it like this:

splat = require('allong.es').splat;

splat(parseInt)(['1', '2', '3']);
  //=> [1, 2, 3]

Splat looks a little different than map because instead of operating directly on an array, it turns a function expecting one argument into a "splatter" expecting an array. And of course, there's a splatWithIndex if that's what you need, although your should probably just use .map for that.

WHy the crazy idea of using a splatter instead of a mapping method or function? This is a bit of a digression, but when you have a function that takes a single argument, you can combine it with almost any other function that takes one argument, like fluent or maybe from the allong.es library, or perhaps debounce from the Underscore library.

second, use a safer function

The allong.es library includes a very handy "variadic" function for turning a function that takes one argument into a function that takes more than one argument. It also includes unary for turning a function that takes more than one argument into a function that takes one argument, it looks like this:

function unary (fn) {
  fn = functionalize(fn);

	if (fn.length == 1) {
		return fn;
	}
	else return function (something) {
		return fn.call(this, something);
	};
} 

We use it like this:

unary = require('allong.es').unary;

['1', '2', '3'].map(unary(parseInt));
  //=> [ 1, 2, 3 ]

allong.es also includes the applyLast function for partial application. It takes any function and lets you bind a value to the last parameter it expects. We can use this with functions like parseInt like so:

applyRight = require('allong.es').applyRight;

['1', '2', '3'].map(applyRight(parseInt, 10));
  //=> [ 1, 2, 3 ]

This works because we've bound 10 to the radix parameter, so the additional parameters that map supplies are ignored.

JavaScript can have some surprises for us, but it has an inherent functional flexibility that allows us to make our own sandpaper to smooth out its knots and whorls.


My recent work:

JavaScript AllongéCoffeeScript RistrettoKestrels, Quirky Birds, and Hopeless Egocentricity


(Spot a bug or a spelling mistake? This is a Github repo, fork it and send me a pull request!)

Reg Braithwaite | @raganwald

You can’t perform that action at this time.