On function composition in JavaScript
06 Mar 2024 (javascript, functional-programming)
In 2020, there was a proposal to add a “pipe” operator to JavaScript. It is currently in the draft stage, but its syntax has changed substantially from the original proposal. It most closely represents the pipe operator from the language Hack.
The new operator (|>
) would not fundamentally add new
functionality to JavaScript, but would provide an alternative syntax for
chaining function calls. The following expressions would be
equivalent:
|> f(%) |> g(%) == g(f(x)) x
The most obvious advantage of the left-hand expression is that it represents chained function execution as a clear pipeline of function calls (hence the name), from left to right, which is more intuitive and readable than unravelling a cluster of nested function calls from the inside out. Particularly in cases where lots of functions with long names are being composed, the expression can get quite unwieldy. E.g. something like
mapStructure(validate(JSON.parse(json)))
In this case, the expression would most likely be split into multiple smaller ones, with intermediate values stored in variables for clarity, like
const obj = JSON.parse(json);
const validated = validate(obj);
const mapped = mapStructure(validated);
But the pipe operator would provide an alternative syntax that arguably makes it clearer what is happening because variables are not introduced into the scope which might be used multiple times. The “pipelined” version of the above would be
|> JSON.parse(%) |> validate(%) |> mapStructure(%) json
However, the original proposal would have more closely resembled the F# forward pipe operator. The previous snippet in the F# style would be
|> JSON.parse |> validate |> mapStructure json
There are pros and cons of each, but I have to say, I prefer the F#
version. The placeholder operator %
introduces indirection
and bloat where the purpose of the proposal is to do the opposite. But
regardless of the specifics of the implementation, finding out about
this proposal got me thinking about composition in JavaScript more
generally, and why this proposal has only come about now.
Methods vs functions
There is already a mechanism in JavaScript for the chaining of certain behaviours that follows a similar structure: object methods.
JavaScript does not have classes, but it uses a prototypal
inheritance model to associate pieces of functionality with certain
types of object—these are methods. For example, we might create an
object type called Person
and attach a greet
method to it like so:
function Person(name) {
this.name = name;
}
.prototype.greet = function() {
Personreturn `Hello, ${this.name}.`;
}
Now if I create an instance of Person
for myself
const me = new Person('Hadrian');
the greet
method will use my name without having to take
an argument
.greet(); // => "Hello, Hadrian." me
This means that a method is essentially a partially applied function.
The greet
example above is similar to a function which
takes a name, then returns another function which greets that
name.
const greet = name => () => `Hello, ${name}.`;
const greetMe = greet('Hadrian');
greetMe(); // => "Hello, Hadrian."
The greet
method returns a string, and in JavaScript,
strings are also objects with associated methods. Therefore, we could
chain the greet
method with a method from the
String
prototype.
const isGreeting = me.greet().startsWith('Hello'); // true
The Array
prototype methods are some of the most
commonly used, and they lend themselves to being chained together
particularly well because they are methods on Array
which
also return an Array
. We might perform a series of array
transformations by chaining methods, like so:
const people = [{ name: 'John' }, { name: 'Jane' }, { name: 'Harry' }, { name: 'Jane' }];
// Return a list of unique names that start with 'J'
const uniqueJNames = people
.map(person => person.name)
.filter(name => name.startsWith('J'))
.reduce((acc, name) => acc.includes(name) ? acc : [...acc, name], []); // => ['John', 'Jane']
As long as the behaviour you want to compose is encoded in prototype methods, JavaScript allows you to easily create clear, pipeline-esque chains of functionality—but that’s a big if.
Why now?
JavaScript is not the most principled language. In most cases, if you don’t like a programming language, you can just use a different one; even in the case of Java, which is king of the JVM, there are other languages that can nonetheless exist in that environment, like Clojure and Scala.
On the other hand, JavaScript is the language of the browser, and as such, when the programming zeitgeist moves on to a new paradigm, pressure grows for JavaScript to incorporate elements of that paradigm. As a result, JavaScript is a bit of a Frankenstein. As previously mentioned, JavaScript doesn’t have classes, but it does feature class syntax which gives the programmer the option of writing code in a way that feels object-oriented. In truth it is syntactic sugar over the existing prototypal inheritance model.
In recent years, certain aspects of functional programming have
become popular, like lambda expressions, immutability and functions as
first-class entities. ES6 formally introduced these concepts into
JavaScript with new syntax, like arrow functions and the
const
and let
keywords, and they have become
central elements of the language—so much so that var
is
rarely seen and arrow functions might be more widely used than the
traditional function
syntax.
Prototypal inheritance, by comparison, has fallen out of favour.
Although standard prototype methods like the aforementioned
Array
methods are used as much as ever, it’s rare to see
custom methods being attached to prototypes in a modern codebase.
Instead, functionality is typically encoded into ordinary functions.
This explains why the proposal for a pipe operator has only appeared
quite recently: prototypal inheritance and the optional class syntax are
JavaScript’s current options for composition, and we’re not using them.
However, technically speaking, the pipe operator is not for function
composition, but function application.
What is really missing?
As previously mentioned, JavaScript’s implementation of lambda expressions (which it calls arrow functions) are used very widely—arguably a little too widely. Although lambda expressions are a feature of functional languages, these languages tend not to promote using them too frequently; they are a tool for encoding particularly unique logic that is unlikely to be repeated. It is usually better to represent complex logic as an expression of simpler functions—by composing them.
Often the syntax for lambda expressions is ugly, and this is my own conjecture, but I suspect it is intentional to discourage using them. The epitome of this can be found in Python:
= lambda x : x + 1
inc 1) # => 2 inc(
If you really want a lambda expression you’d better spell it out.
In JavaScript, even if we want to compose functions, arrow functions are how we have to do it because there is no built-in way to compose functions independently of their inputs. As previously mentioned, the proposed pipe operator is inspired by Hack, but it is not too dissimilar to the F# forward pipe. But F# has another operator specifically for composing functions. Using this operator we could encapsulate the JSON example from earlier into a named function like so:
let handleJson = JSON.parse >> validate >> mapStructure
There is no need to make any reference to the json
variable that will be passed into the function—the functions can be
composed independently from their inputs. To do the same thing in
JavaScript would require an arrow function, meaning the argument that
will be ultimately passed to the function has to be handled
explicitly:
const handleJson = json => mapStructure(validate(JSON.parse(json)));
In my opinion, the proposed pipe operator wouldn’t add much utility to JavaScript, but a true composition operator would be a welcome addition.
Existing solutions
There are JavaScript libraries which attempt to solve this problem.
For example, Ramda includes a pipe
function which composes
functions just like the F# compose operator. For example, to repeat the
previous example of parsing and mapping a JSON structure, the equivalent
using Ramda would be the following snippet.
const handleJson = R.pipe(JSON.parse, validate, mapStructure);
This does result in a similarly clean structure that resembles a pipeline—it works well. However, it isn’t obvious in which direction the functions are evaluated. It so happens that the functions will be evaluated from left to right, but there is nothing about the syntax that makes that obvious. This wouldn’t be any less confusing to someone with a background in a functional language like Haskell, because functional languages vary in their direction of composition. In Haskell, composed functions are evaluated from right to left, like so:
= mapStructure . validate . JSON.parse
handleJson -- equivalent to
-- handleJson json = mapStructure (validate (JSON.parse json))
One advantage of the F# composition operator is that it resembles an arrow, making the flow of execution abundantly clear.
At the end of the day, JavaScript is not a functional language; it’s a hodgepodge of different paradigms, one of them being functional. Ideally, a language would have strong principles, meaning updates and feature proposals would rarely be necessary. But JavaScript has a specific role in web development that incentivises it to change with the times—if a new feature would improve the way we currently use the language, then it is a good change. With that said, the pipe operator proposal in its current state is a marginal improvement, if an improvement at all. I think a true composition operator would be a much more valuable addition to the language.