Point-free gotchas in JavaScript
I am a big fan of functional programming, I enjoy the conciseness of it and it fits my way of thinking better. I also like clean code with as few redundancies as possible. Having said that, it should come as no surprise that point-free (sometimes also called tacit) style appeals to me. Over the last few days I came across several gotchas when applying this style in JavaScript and decided to write them down.
What is point-free style
As Wikipedia states:
[Point-free] is a programming paradigm in which function definitions do not identify the arguments (or “points”) on which they operate.
This may seem weird at first, but let’s use a simple example. Assume we have a function that takes a string and returns that string with the first letter capitalised. Next, we have an array of strings that we all want to capitalise. This is a simple use case for the map
function:
const capitalise = str =>
str && str.charAt(0).toLocaleUpperCase() + str.substr(1);
const words = ["foo", "bar", "baz"];
// logs [ 'Foo', 'Bar', 'Baz' ]
console.log(words.map(w => capitalise(w)));
// logs [ 'Foo', 'Bar', 'Baz' ]
console.log(words.map(capitalise));
Notice the second map
use, it does not state the name of the argument and does not create a new function. The reason this works is that map
calls its first argument as a function taking three arguments:
- the item of the array to process (this is the only mandatory parameter),
- the index of that item,
- the whole array being processed
Out capitalise
function happens to also take the item to be processed as its first (and only) argument and so it works when used point-free in this case.
There are more uses for this style and we will see them as we go through the article.
Gotcha #1: Function taking more parameters than you expected
The first gotcha comes from the fact that you can call a function in JavaScript with as many arguments you want — be it too few or too many.
In case you provide too few arguments, those you haven’t provided are set to their default value (which is undefined
unless otherwise specified).
In case you provide too many arguments, the function ignores the excessive ones (unless it uses the arguments
object).
This is probably not new to you, in the context of point-free it can lead to some unexpected results, though.
Let’s take the simplest of examples: write a function that takes an array of strings and returns the numeric values of the items. For the sake of an example we assume the input is correct. Simple enough, there is Number.parseFloat
for that:
const nums = ["25", "45", "11"];
// logs [ 25, 45, 11 ]
console.log(nums.map(num => Number.parseFloat(num)));
// logs [ 25, 45, 11 ]
console.log(nums.map(Number.parseFloat));
As we can see, the point-free version works like a charm.
Well, what if someone told us the numbers are always integers and we don’t have to parse them as floats? Then we would swap the Number.parseFloat
for the Number.parseInt
, right?
// logs [ 25, 45, 11 ]
console.log(nums.map(num => Number.parseInt(num)));
// logs [ 25, NaN, 3 ]
console.log(nums.map(Number.parseInt));
Whoa, what is that? The point-free version behaves rather strange all of a sudden.
The reson for this is that while Number.parseFloat
only takes one argument – the string to parse – Number.parseInt
takes an additional optional argument – the radix of the number to be output (for example 16 for hexadecimal strings). Thus when used in a map like that this is what actually happens:
console.log(nums.map((item, index, array) =>
Number.parseInt(/* string: */item, /* radix: */index, array)));
As we can see the radix argument of Number.parseInt
is set using the index of the current item. That explains the 3
output for the 11
input as 3 is 11 in binary.
This is the first type of issue that can arise from point-free in JavaScript: functions taking more arguments than you expect.
There is no fool-proof way to protect yourself against this other than using point-free only with functions you know the signature of and know are not going to change, otherwise your code can break unexpectedly.
Gotcha #2: Unexpected this
This one popped up in a job interview I took not too long ago:
const obj = {
message: "Hello",
getMessage() {
console.log(this.message);
},
};
// Broken
setTimeout(obj.getMessage, 0);
The question was to fix the error.
One would probably expect "Hello"
to be output (I know I did). Yet, undefined
is output to the console.
The reason for this is the way setTimeout
executes the callback function. The callback is executed in a different execution context and if this
is not set explicitly, it will be set to the global
object. And as global
(or window
if run in browser) does not have a message
member our example prints undefied
.
There are two way to fix this:
// Fix A - closure
setTimeout(() => obj.getMessage(), 0);
// Fix B - binding
setTimeout(obj.getMessage.bind(obj), 0);
The first one uses a closure to implicitly set this
of the getMessage
call to the proper value.
The second (point-free) one makes use of the bind method to set the value of this
explicitly.
There is another example of a code that seems to be alright — simple regular pattern use:
const isActivationCode = /^\d{4}-\d{4}-\d{4}$/.test;
console.log(isActivationCode("1234-5678-1234"));
However this ends up throwing a TypeError
saying:
Method RegExp.prototype.test called on incompatible receiver undefined
or a bit more helpfully in Safari:
RegExp.prototype.test requires that |this| be an Object
Again, the problem is that this
has an unexpected value (in this case undefined
). The solutions are the same as in the previous case:
// Fix A - closure
const isActivationCodeClosure = code => /^\d{4}-\d{4}-\d{4}$/.test(code);
// Fix B - binding
const regex = /^\d{4}-\d{4}-\d{4}$/;
const isActivationCodePointFree = regex.test.bind(regex);
// logs true
console.log(isActivationCodeClosure("1234-5678-1234"));
// logs true
console.log(isActivationCodePointFree("1234-5678-1234"));
The point to take here is that if the function you want to call point-free makes use of this
, you should be very aware that it is set to what you expect.
Conclusion
As much as point-free style is useful in other (functional) languages, in JavaScript it often brings problems that might not be worth the conciseness it brings. I still use it sometimes when the function called is under my control. After these experiences I will be more careful with it, though.