Higher-Order Functions that Simplify Pure Functions

Giancarlo Radaelli
JavaScript in Plain English
8 min readJan 18, 2022

--

In this article I describe a pattern for implementing pure functions that emphasises the declarative style.

It is based on two new higher-order functions placed inside basic prototypes in order to have them available (using dot notation) for all objects and functions.

Higher-order functions

In the following, I will use the acronym HOF for Higher-Order Function: a function that has one or more functions among its input parameters and/or produces a new function as a result of its invocation.

Pure function implementation

Pure functions are essentially a sequence of elementary transformations which, starting from the input parameters and passing through a sequence of intermediate results, produce the final result.

Procedural implementation

In the general case, transformations need, as input, a subset of the intermediate results generated by the transformations that precede them in the sequence, (in addition to a possible subset of the function’s input parameters).

So the function is split into a set of procedural steps that store results inside local variables.

Implementation with a chain of functions

In most cases, it is possible to simplify the above scheme and implement the function as a single tube of transformations where the output of one of them is the input of the next one.

If transformations are defined inline the input parameters of the pure function are available inside transformation closures.

To maximize the applicability of this schema, transformations, if needed, can propagate previous results to the downstream inserting the input inside the result (in this case the result is a tuple or a map).

A tube of functions (transformations)

With JavaScript there are two different ways to concatenate functions:

  • The dot notation which through the use of predefined methods, allows the concatenation of transformations that produce new strings or new arrays (but we will see a solution for objects as well).
  • A HOF that takes functions as input and combines intermediate results and function invocations. This HOF is usually called pipe.

The two techniques can be mixed together to obtain the best implementation.

I will start describing the second alternative.

Postfix composition (the pipe function)

A chain of functions can be created using the mathematical concept of function composition:f( g( h(o) ) ). This can be directly interpreted as JavaScript code, but there are three main problems with this approach:

A lot of nested levels. Nested levels are difficult to read.
A simple solution is the definition of a HOF that returns the composed function and then apply it on the starting object:
compose(f,g,h)(o)

The definition of the compose HOF is very simple:
compose = (...fs) => o => fs.reduceRight((a, f) => f(a), o)

The first transformation applied to the input object (h) is the last in the list of the compose parameters.

In mathematics, you have a similar problem. It can be avoided adopting the so-called Reverse Polish Notation (or postfix notation): operands are listed before operators. E.g. 5 + 3 is written 5 3 +. Likewise f( g( h(o) ) ) can be written o h g f (in the postfix notation brackets are not used).

In JavaScript we define the HOF pipe that, like the RPN, reverses the order of operators:
pipe = (...fs) => o => fs.reduce((a, f) => f(a), o)

Now we can write: pipe(h,g,f)(o)

The operand o is on the wrong side of the function tube.

The parameter o should be positioned to the left of the transformation sequence: pipe(h,g,f)(o) // wrong side.

This problem will be solved later, after we discuss the usage of the dot notation.

As explained above the pipe function is essentially a postfix notation for functions composition, so I prefer the name postfix composition for it.

Dot notation to invoke pure functions

Dot notation is used to invoke class methods and here we want to build just pure functions (no mutations).

If we use only methods that do not mutate objects we can still implement pure functions. In this case, dot notation can be interpreted as syntactic sugar for calling functions that takes an object as first parameter.

Following this line of thought, it would be interesting to be able to invoke any function that has an object as its first parameter, using dot notation.

f = (o, arg1, arg2, ...)    o.f(arg1, arg2)   // not possible

On the other hand, this is already possible by defining the function as a method of a class and the object as an instance of that class…

class MyObj { f(arg1, arg2)  { console.log(arg1, arg2) }}
o = new MyObj()
o.f('arg1', 'arg2')

But the point is to be able to do it without falling into the swamp of object-oriented programming (I know, I am ungrateful towards OOP but it is in the spirit of this article).

Unfortunately, this is not possible… but something similar can be implemented.

Invoke HOF

To emulate the dot invocation of a function, it is possible to define something similar to the pipe function and make it callable from any kind of object, using the dot notation:

Object.defineProperty(
Object.prototype,
'invoke’,
{
value:i
function(...fs) {
return fs.reduce((a, f) => f(a), this)
},
enumerable: false
}
)

So even if it is not possible to write o.h.g.f it is possible at least write this:

o.invoke(h).invoke(g).invoke(f)    
// or
o.invoke(h,g,f)

Now the parameter o is placed on the correct side of the function tube:

pipe(h,g,f)(o)  o.invoke(h,g,f)     

Perimeter and Area of a rectangle (pipe example)

To illustrate the concepts expressed above, let’s consider a function that transforms an orthogonal (vertical or horizontal) rectangle into a string containing the rectangle’s area and perimeter.

The end result is not very useful, but let’s focus on the implementation of the function.

A vertical/horizontal rectangle can be represented by two points: the two opposite vertexes. A point can be represented by a vector of numbers:

rect = {a: [2, 6], b: [5, 2]}

The first transformation retrieves the sides:

const sides =
r => [Math.abs(r.a[0] - r.b[0]), Math.abs(r.a[1] - r.b[1])]

The second transformation adds the perimeter and the area to the resulting tuple:

const  addPerimeterAndArea =
sides => [...sides, 2*(sides[0]+sides[1]), sides[0]*sides[1]]

The third transformation creates the output string:

const rectPropsToString =
rectProps => `
sides: ${rectProps[0]}, ${rectProps[1]}
perimeter: ${rectProps[2]}
area: ${rectProps[3]}
`

Now we are ready to define the function as a single tube of transformations:

const rectToString = 
rect => rect.invoke(sides, addPerimeterArea, rectPropsToString)
console.log(rectToString({a: [2, 6], b: [5, 2]})) sides: 3, 4
perimeter: 14
area: 12

Median (dot notation example)

Given a vector of numeric values the median is the central value of the sorted vector (the mean of the two central values when the length of the vector is even).

The function can be implemented as a tube without the need of the invoke function, but the extraction of the central values is a little complex so it is better to use an evocative name for it.

const extractCentralValues =
v => v.slice(div2(v.length>>1 - 1, v.length>>2 + !v.length%2)

Integer division by 2 in binary is like the division by 10 in base-10: just skip the last digit. In binary, this is simply achieved with the sign-propagating right shift operator (>>).

An implementation variant, that uses evocative names and performs the division by two only once, makes use a higher-order IIFE (Immediately Invoked Function Expression), in bold in the code snippet.

const extractCentralValues =
((middleIndex, evenLength) =>
v => v.slice(middleIndex, middleIndex + 1 + evenLength)
)(v.length>>1 - 1, !v.length%2)

It is possible to define middeIndex and evenLength as functions and, in order to perform the division (by two) once, use a memoized version (with a single value cache) of middleIndex , but in these cases I prefer IIFEs.

The median function can now be defined as a tube:

const median = v => 
v.sort()
.invoke(extractCentralValues)
.reduce((a, e) => a+e/2, 0)

Yeah, the reduce can be replaced by a function with an evocative name:

const meanValue = v =>
v.reduce((a,e) => a + e)/v.length

At this point we have two alternatives to list the transformations (I prefer the first one):

const median = v => 
v.sort()
.invoke(extractCentralValues)
.invoke(meanValue)
orconst median = v =>
v.sort()
.invoke(
extractCentralValues,
meanValue
)

Bind extra arguments (bindArgs HOF)

In case a transformation needs the additional input parameters of the main function, the transformation should be defined inline, so they are available in the closure. But this solution precludes the possibility to use evocative names.

Fortunately, there is an alternative: we can partially applicate the transformation.

A partial application binds some parameters of a function to fixed values and returns a new function that takes fewer input parameters. In our case only the first parameter remains free:

Object.defineProperty(
Function.prototype,
'bindArgs',
{
value:
function(...args) {
return o => this(o, ...args)
},
enumerable: false
}
)

The new partially applied function in the above code is in bold. Note how args values are frozen into its closure.

Rotation of a point (bind example)

The rotation of a (cartesian) point by an angle alfa (in radians) can be obtained by transforming the point into polar coordinates, adding the angle to the polar angle and reversing the transformation into cartesian coordinates.

Yeah, the most efficient way to rotate a point is through the use of a rotation matrix … but this is just an example to demonstrate the use of the bind function.

const toPolar = 
p => [Math.sqrt(p[0]*p[0]+p[1]*p[1]), Math.atan2(p[1],p[0])]
const polarRotation = (p, angle) => [p[0], p[1] + angle]const toCartesian = p => [p[0]*Math.cos(p[1]), p[0]*Math.sin(p[1])]

The polarRotation function takes 2 arguments … the second must be bound before using the function in a function tube.

The rotate function is:

const rotate = (p, angle) =>
p.invoke(
toPolar,
polarRotation.bindArgs(angle),
toCartesian
)

Recap

After presenting a model for the implementation of pure functions (the tube model), I have defined two HOFs (invoke and bindArgs) that allow this model to be applied to the implementation of most of them. Three elementary examples clarify how to use it.

More content at plainenglish.io. Sign up for our free weekly newsletter. Get exclusive access to writing opportunities and advice in our community Discord.

--

--