Have you ever heard of point-free coding? What is it? When should you use it?
In this article, I will discuss point-free styling, including:
- What point-free programming is
- Practical examples of point-free codes
- The benefits of point-free styling
- How to incorporate point-free styling in your code
- Disadvantages of point-free programming and when not to use it
Point-free programming: Definition
Point-free styling (also known as Tacit programming), is a programming paradigm in which the function definitions do not identify or take the arguments (also known as “points”) on which they operate. Instead, the definitions are composed of other functions themselves, among which are combinators that manipulate the arguments.
In other words, tacit programming functions don’t take arguments as we know them, they take other functions and these are the functions, in the end, that take the arguments.
Point-free code: a practical example
As usual, words are but wind. So, here’s an example of a relatively common way of writing a function that returns whether a number is even or not:
const isEven = number => number % 2 === 0;let myNumbers = [1, 2, 3, 4, 5, 6];console.log(myNumbers.map(myNumber => isEven(myNumber)));// will output: [false, true, false, true, false, true]
Refined into Tacit programming, this should become:
const isEven = number => number % 2 === 0;let myNumbers = [1, 2, 3, 4, 5, 6]console.log(myNumbers.map(isEven));// will output: [false, true, false, true, false, true]
Wait, what? What’s going on?
Note how the point-free function call does not state the name of the argument nor create a new function. The reason why this works is that
map calls its first argument as function, taking the following arguments:
- The item of the array to process: the only mandatory parameter
- The index of that item
- The array itself
isEven 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.
Benefits of point-free programming
All programs, applications, and code software must follow this basic rule: the code needs to be easy to read, understand and maintain. In other words, it needs to be as simple as it can be.
Programs should be written for people to read, and only incidentally for machines to execute. — Harold Abedson in Structure and Interpretation of Computer Programs
According to this principle, an application that uses redundant arguments may lead to reading and understanding issues.
No variables to name
There are only two hard things in Computer Science: cache invalidation and naming things — Phil Karlton (Netscape engineer)
Coming up with names for variables and functions can be unexpectedly hard. With point-free styling, you can save yourself some time and effort.
Naming variables explicitly can help you think about abstractions and improve your code design.
Given that code is read many more times than it is written, not introducing unnecessary noise in the form of variables may be a very good thing.
One benefit of having all arguments explicitly referred to is easier debugging.
You can simply
console.log parameter values and get an understanding of what is happening in the system in a given line of code. You can easily detect where your code is draining.
Pipe and compose
Let’s complicate things a little.
compose are types of point-free styling that adds extra power to your code by nesting operations.
compose is essentially a function that takes two functions as parameters, and runs one after the other:
const compose = (f, g) => x => f(g(x));
This is the result of
g(x) that is passed to
Applied to something more digestible, it may look like:
const add = (a, b) => a + b;
const mult = (a, b) => a * b; add(2, mult(3, 5));
The arithmetic operation above is calculating:
2 + 3 * 5. As you may know, multiplication has priority over addition. So you start by calculating
3 * 5 and add
2 to the result. This is a form of function composition since it is the result of the multiplication that is passed to the
pipe is an intuitive way to read functions in the order they are going to be called (hence the name). It works exactly as
compose but applies the functions left-to-right using the
const pipe = (...fns) => input => fns.reduce((mem, fn) => fn(mem), input);
Given the previous
pipe we can create three dummy functions to make some simple operations such as:
const pipe = (...fns) => input => fns.reduce((mem, fn) => fn(mem), input);const double = x => x * 2;
const addOne = x => x + 1;
const square = x => x * x;
At this point, we can proceed as follows:
pipe(square, double, addOne)(2)
// same as compose(addOne, double, square)(2)
You may notice that this composition of functions is closely related to Closures and Currying. Indeed, these types of operations and composing are also forms of currying.
No need to sweat, there are libraries that handle this kind of function composing by themselves, like Rambda (
pipe) or Lodash (
pipe). However, it’s always useful to know what's happening under the hood to understand the flow, which will allow you to debug, detect and fix potential issues.
Caveats of point-free programming
There are cons or caveats to take into account when overusing point-free styling.
Readability may suffer
One of the obvious downsides to point-free styling is that it can cause readability issues if taken to the extreme, for example, by chaining functions and arguments along with an application. It’s easy to lose track of the code if you abuse these methods.
If the code is only understandable by the owner, the application is sentenced to death and it will create a handicap in the code because it will be extremely dependant on one person (or a few).
I won’t dive much into detail on
this basically depends on the caller, and, while chaining functions, the caller is the first triggered function so if you are using
this in a composed function, it will lose the scope.
While using built-in functions, we may thrive on some unexpected behaviors. Given one of the previous snippets:
let myStringNumbers= ['1', '2', '3'];console.log(myStringNumbers.map(number => parseInt(number))); // will output [1, 2, 3]
However, when refined to point-free, it results in:
let myStringNumbers= ['1', '2', '3'];console.log(myStringNumbers.map(parseInt));// will output [1, NaN, NaN]
This happens because
map is passing more than one argument to the
parseInt. As we’ve seen before, the
- Passes the current value
- Passes the
- Passes the array itself
parseInt only accepts a string and the radix (the base in mathematical numeral systems). To fix it, it’s a common practice when dealing with loops and point-free functions to use the spread operator along with an auxiliary function which will allow the function to take only the first argument and ignoring the rest:
let myStringNumbers= ['1', '2', '3'];const unary = fn => (...args) => fn(args);console.log(myStringNumbers.map(unary(parseInt))); // will output [1, 2, 3]
Point-free programming is a code-style choice and it’s not essential to make your code more functional. Understanding this concept, however, makes reading and discussing other people’s code easier.
As with many things in life (and especially in programming where we like to overkill things), overusing it might introduce more problems than it solves, and you need to trust your intuition (and experience) on where the balance is.
Keep it reasonable and ponder whether removing an explicit variable reference makes the code easier or harder to read. Use point-free style in moderation and make your code easier to read and follow.
I still use it sometimes when the function called is under my control. After experiencing its downsides, however, I will be more careful with it.
There’s no evidence that demonstrates higher performance compared to other patterns. However, there is a clear trend in the history of programming towards higher abstractions… and an equally clear history of resistance to this trend.
What do you think? Let me know in the comments below!