loading...

Point-free gotchas in JavaScript

danhomola profile image Dan Homola Updated on ・5 min read

Note: this post was originally published on my Medium profile

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 reason 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.

Posted on by:

danhomola profile

Dan Homola

@danhomola

Front End dev, art and music lover, functional programming aficionado

Discussion

markdown guide
 

Thanks for the article Dan!

I believe the solution to your first gotcha is "unary" (e.g. ramdajs.com/docs/#unary) - something like:

const R = require('ramda')

const nums = ["25", "45", "11"]
console.log(nums.map(R.unary(Number.parseInt)))
// logs [ 25, 45, 11 ]

I am a little confused with how your second gotcha (this losing context) is related specifically to the pointfree style. I thought this is a general issue with this and not specifically tied to pointfree. Your first example in the second gotcha also suggests that (no pointfree there). Am I missing something?

 

Thanks for the comment! The unary function seems neat, I will definitely give Ramda a closer look :)

As for the second gotcha it is simply to illustrate that () => foo() cannot always be replaced by foo as a more "point-free" style. I hope it is clearer now.

 

Thanks for this well written article. It describes exactly the reason why I prefer closures over point-free functions. The short notation of point-free looks great, but the lack of control over the scope as well as the arguments passed in has caused many flaws in my Javascript code over the years.

 

Thank you for your kind comment, I'm glad you liked it :)

 

In your bind example, it might be worth mentioning a related gotcha: bind sends arguments as additional parameters. In your getMessage example, if getMessage took a number, and you bound from a function with an unrelated number param, you get a nasty surprise!

 

Thanks for the addition, I did not realise that!

 

'Fix B' from above could be expressed slightly more elegantly as:

// Fix B - binding
const isActivationCodePointFree = RegExp.prototype.test.bind(/^\d{4}-\d{4}-\d{4}$/);