DEV Community

loading...

Point-Free Style (in Javascript)

justinmmorgan profile image Justin Morgan ・Updated on ・13 min read

All the cool-kids are talking about point-free style. They brag about how clean and declarative their code is and look down at lowly imperative code. You glean that it has something to do with functional programming and clever use of functions as first-class values, but what does it all mean? You don't want to be the last one picked for the coder kick-ball team, do you? So let's dive in and see what it's all about.

In an earlier entry (A Deeper Dive into Function Arity), I alluded to data-last signatures and a point-free style. Although there were occassionally examples, I feel it would be of value to go into greater detail about what these terms mean, and what advantages they afford us. I will not rely too greatly on the contents of that article.

As an introductory definition, point-free style is passing function references as arguments to other functions. A function can be passed as an argument in two ways. Firstly, an anonymous function expression (or declaration) can be provided inline:

    // Function declaration 
    function (arg1, arg2) { ... }
    // Newer (ES2015) style - unnamed function expression
    (value) => { ... }

    // Example
    doSomeThingThatResolvesToPromise
        .then((valueFromPromiseResolution) => {...})
        .catch((errorFromPromiseRejection) => {...})
Enter fullscreen mode Exit fullscreen mode

While this works, it isn't point-free style. A function expression has been declared inline to the function which will consume it. Instead, if we declare our function separately, assign it a name, and provide it by reference to another function:

    function somePromiseValueResolutionHandler(value) { ... }
    function somePromiseValueErrorHandler(error) { ... }
    // Or, using function expressions:
    // const somePromiseValueResolutionHandler = value => {...}
    // const somePromiseValueErrorHandler = error => {...}

    doSomeThingThatResolvesToPromise
        .then(somePromiseValueResolutionHandler)
        .catch(somePromiseValueErrorHandler)
Enter fullscreen mode Exit fullscreen mode

With these examples, you're only seeing the bare minimum requirement of point-free style. A function is being passed by reference as an argument to a function where it expects a callback. The referenced function's signature matches the function signature expected by the callback, and thereby allows us to pass the function reference directly. This allows our function chains to have a lot of noise removed, as functions are not defined inline and the arguments from one function are passed implicitly to the referenced function. Consider:

function someAsynchronousAction(arg1, arg2, (error, successValue) => {...})
// versus
function thenDoTheThing (error, successValue) { ... }
function someAsynchronousAction(arg1, arg2, thenDoTheThing)
Enter fullscreen mode Exit fullscreen mode

At this point, you may be thinking "yeah, that looks a little nicer, but is it really worth the effort?" Broadly speaking, this style of code flourishes when you embrace:

  1. knowledge and patterns of function arity, and
  2. utility functions.

Function Arity Patterns

I have written elsewhere more substantively on the topic of function arity. For the purposes of this discussion, it is sufficient to know that the term arity refers to the number of parameters a function signature contains. Functions can be said to have a strict arity when they have a fixed number of parameters (often given a latin-prefixed name such as unary and binary) or variadic when they can receive a variable number of arguments (such as console.log, which can receive any number of arguments and will log each argument separated by a space).

In Javascript, all functions will behave as variadic functions technically. Although scoped variables can capture argument values in the function signature, any number of arguments are collected in the arguments array-like object (or captured with another name using the rest operator) without any additional steps taken.

function variadicFunction1() {
  console.log("===Arguments Object===");
  Array.from(arguments).forEach((arg) => console.log(arg));
  return null
}
function variadicFunction2(a, b) {
  console.log("===Declared Parameters===");
  console.log(a);
  console.log(b);
  console.log("===Arguments Object===");
  Array.from(arguments).forEach((arg) => console.log(arg));
  return null
}

variadicFunction1("a", "b", "c")
// ===Arguments Object===
// a
// b
// c
// null

variadicFunction2("a", "b", "c")
// ===Declared Parameters===
// a
// b
// ===Arguments Object===
// a
// b
// c
// null

variadicFunction2("a")
// ===Declared Parameters===
// a
// undefined
// ===Arguments Object===
// a
// null
Enter fullscreen mode Exit fullscreen mode

The reason the functions return null is to distinguish the final return of undefined referencing the b argument in variadicFunction2 from the otherwise default undefined return value of the function (which is a void function, i.e. returning undefined as Javascript does by default when no value is explicitly returned).

Related to this point, and essential for the topic at hand, is that in Javascript all function references are technically variadic (i.e. accepting any number of arguments without erroring) though their behaviour remains constrained by however the function signature is defined. That is, we can pass functions by reference as arguments, without writing the execution/assignment of arguments section as so:

function add(a, b) { return a + b }
function subtract(a, b) { return a - b }
function multiply(a, b) { return a * b }
function divide(a, b) { return a / b }

function operation(operator) { 
  // Take all but the first argument
  let implicitArguments = Array.from(arguments).slice(1) 
  // Same thing using rest operator
  // let [operator, ...implicitArguments] = [...arguments] 

  // spread the array arguments into the function execution
  return operator(...implicitArguments) 
}

operation(add, 10, 20)
// operation executes add(10, 20)
// 30
operation(multiply, 10, 20)
// operation executes multiply(10, 20)
// 200
operation(multiply, 10, 20, 40, 50, 20, 50)
// operation executes multiply(10, 20, 40, 50, 20, 50) 
// but the multiply function ignores all 
// but the first two arguments
// 200
Enter fullscreen mode Exit fullscreen mode

This behaviour does pose a challenge, as function arity is not strictly enforced. You can do unusual things and your code will continue to function without erroring. Many developers exploit this characteristic but this requires mentally-retaining more implicit knowledge of the system than if the function arity were explicitly stated and enforced.

An example where this behaviour is exploited is in the Express framework middleware/callback function, which can have multiple signatures. See Express documentation for app.use

// `Express` callback signatures 
(request, response) => {...}
(request, response, next) => {...}
(error, request, response, next) => {...}

// From the Express documentation 
// Error-handling middleware

// Error-handling middleware always takes four arguments. You 
// must provide four arguments to identify it as an error-
// handling middleware function. Even if you don’t need to use 
// the next object, you must specify it to maintain the 
// signature. Otherwise, the next object will be interpreted 
// as regular middleware and will fail to handle errors. For 
// details about error-handling middleware, see: Error handling.

// Define error-handling middleware functions in the same way 
// as other middleware functions, except with four arguments 
// instead of three, specifically with the signature (err, req, res, next)): 

Enter fullscreen mode Exit fullscreen mode

Employing this pattern, we can see that we can write our middleware/callback function outside of the site where it will be consumed so long as we match the arity/function signature properly. Refactoring the example from the Express documentation

app.use(function (req, res, next) {
  console.log('Time: %d', Date.now())
  next()
})

// ...can be re-written as 

function logTime(req, res, next) {
  console.log('Time: %d', Date.now())
  next()
}

// ..then hidden away in a supporting file and imported 
// --or hoisted from the bottom of the file-- 
// and passed by reference at the call-site

app.use(logTime)
Enter fullscreen mode Exit fullscreen mode

In currently popular libraries and frameworks such as Express, we implicitly consider the impact of function arity in our code and develop certain patterns that we must become familiar with. Point-free style requires designing with function arity as a central concern.

Data-Last Functions

A pattern that is central to point-free style is that of data-last function signatures. This pattern emerges from the practice of currying a function. A curried function is a function that always takes and applies one argument at a time. Instead of thinking of a function as taking multiple arguments and then producing a single output, we must think of our function as a series of steps before finally arriving at a "final" value.

For example, consider that we are talking about a function which concatonates two strings:

function concat(string1, string2) {
  return string1 + string2
}
Enter fullscreen mode Exit fullscreen mode

The desired behaviour of this function is to take two arguments (both strings) and return a string. This is a functional unit and it may be difficult to conceive of why you would ever need to pause in the middle, but bear with me. To curry this function, we need to allow it to receive each argument one at a time, returning a new function at each step.

function concat(string1) {
  return function (string2) {
    return string1 + string2
  }
}

// or using a cleaner function expression syntax 

const concat = string1 => string2 => string1 + string2

// Executing this function to "completion" now looks like: 
concat("string1")("string2")
Enter fullscreen mode Exit fullscreen mode

It is typical practice to define javascript functions in a non-curried form and to consume functions from external libraries which are provided in a non-curried form. If you wish to use currying in your code, there are curry utility functions in most of the popular functional programming utility libraries such as [Ramda](https://ramdajs.com/docs) and [Lodash/fp](https://github.com/lodash/lodash/wiki/FP-Guide). These utilities simply wrap your existing (non-curried) function and then receive subsequent arguments individually or in groups until all of the required arguments have been supplied. This offers the flexibility of treating functions as truly curried versions or just providing all the necessary arguments as if it were not curried.

Imagine for a moment that you stuck with the original concat function. You are asked to write a function which takes a list of string values and prefixes each with a timestamp.

// ...without currying
function prefixListWithTimestamp(listOfValues) {
  return [...listOfValues].map(value => concat(`${Date.now()}: `, value))
} 

// ...with currying
const prefixListWithTimestamp = map(concat(timestamp()))
Enter fullscreen mode Exit fullscreen mode

Okay, what just happend. I did cheat (a little). We included the map function (rather than use the method on the array prototype) probably from a utility function but we'll write it out below. It behaves in exactly the same way as the prototype method but it is a curried function which obeys the data-last signature.

const map = mappingFunction => array => array.map(value => mappingFunction(value))
// Equivalent to
const map = mappingFunction => array => array.map(mappingFunction)
// Or some iterative implementation, the details of which are unimportant to our main logic
Enter fullscreen mode Exit fullscreen mode

Additionally we created a small utility around our timestamp value to hide the implemenation details.

What is important is that map is a curried function which receives first a mapping function (a function to be applied to each value in an array). Providing the mapping function returns a new function which anticipates an array as its sole argument. So our example follows these steps:


const prefixStringWithTimestamp = value => concat(`${Date.now()}: `)(string) 
// We can pair this down to...
const prefixStringWithTimestamp = concat(`${Date.now()}: `) // a function which expects a string

const mapperOfPrefixes = array => map(prefixStringWithTimestamp)(array) 
// We can pair this down to...
const mapperOfPrefixes = map(prefixStringWithTimestamp) // a function which expects an array of strings
// prefixStringWithTimestamp is functionally equivalent to concat(`${Date.now()}: `)
map(concat(`${Date.now()}: `))

// Perhaps our timestamp implementation can be a utility. 
// We make timestamp a nullary function, `timestamp()`
const timestamp = () => `${Date.now()}: `

map(concat(timestamp())) // A function which expects an array of strings.

Enter fullscreen mode Exit fullscreen mode

This pattern encourages you to design your functions in such a way that the parameters are arranged from least specific to most specific (said another way, from general to concrete). The data-last name implies that your data is the most concrete detail which will be given to the function. This allows for greater function re-use (via function composition) and is necessary to accomplish a point-free style.

Not sure what the proper ordering of parameters is? Don't worry! You will get a feel for it with time. But if you find a function just isn't working, flip it.


const flip = fn => a => b => fn(b)(a)

Utility Functions

Embracing utility functions is critical to realizing the value of point-free style. By doing so, you will realize that a lot of the code you write is a variant of repetitive patterns that are easily generalizable. Additionally it adds a lot of noise to your code.

For example, it is becoming increasingly popular to "destructure" objects and arrays. In many ways, this is an improvement over prior access patterns and itself removes a lot of noise from your logic. If we take that notion a step further, the same can be accomplished by "picking" properties from an object or "taking" from an array.

const obj1 = { a: 1, b: 2, c: 3, d: 4 }

// Destructuring
const {a, c, d} = obj1

// versus "Pick"

// `pick` (from Ramda): Returns a partial copy of an object
// containing only the keys specified.
// If the key does not exist, the property is ignored.

R.pick(["a", "d"], obj1); //=> {a: 1, d: 4}
R.pick(["a", "e", "f"], obj1); //=> {a: 1}

Enter fullscreen mode Exit fullscreen mode

That little definition already exposes a behaviour that isn't matched by the destructuring approach but is critical: pick accounts (in a particular way) for when the property doesn't exist. Say instead you wanted to change the behaviour to such that a default value is supplied if the property doesn't exist on the original object. Suddenly the destructuring approach will get a lot messier. With utility functions (especially pre-written librariers), we can get accustomed to using different utilities that already provide the behaviour we desire while removing this edge case code from our main logic.

const obj1 = { a: 1, b: 2, c: 3, d: 4 }

const {
  a: a = "Nope, no 'a'", 
  c: c = "No 'c' either", 
  e: e = "I'm such a disappointing object"
  } = obj1

// versus

// `pipe` (from Ramda)
// Performs left-to-right function composition. 
// The first argument may have any arity; the remaining arguments must be unary.
// In some libraries this function is named sequence.
// Note: The result of pipe is not automatically curried.
const f = R.pipe(Math.pow, R.negate, R.inc);
f(3, 4); // -(3^4) + 1

// `merge` (from Ramda):
// Create a new object with the own properties 
// of the first object
// merged with the own properties of the second object. 
// If a key exists in both objects, 
// the value from the second object will be used.

R.merge({ name: "fred", age: 10 }, { age: 40 });
//=> { 'name': 'fred', 'age': 40 }

// Our own derivative utility, `pickWithDefaults`
const pickWithDefaults = (keys, defaults) => R.pipe(R.pick(keys), R.merge(defaults));
// Notice: Our data source is omitted, which if included would be written as
const pickWithDefaults = (keys, defaults) => (object) => R.pipe(R.pick(keys), R.merge(defaults))(object);


const defaultValues = { a: "default a", c: "default c", e: "default e" }
pickWithDefaults(["a", "c", "e"], defaultValues)(obj1); //=> { a: 1, c: 3, e: "default e" }
Enter fullscreen mode Exit fullscreen mode

Now imagine that the destructuring approach taken above is employed throughout the codebase, but you don't realize that it contains a bug and this bug emerges only in a subset of the use cases. It would be pretty challenging to do a textual search of the project and modify/correct them. Now instead consider whether our object property access had been done using a function like pick/pickAll. We now have two courses of corrective action.

The first is to "correct" the behaviour in our implementation by implementing our own version, and then update the imports throughout our project to use the fixed version of the function. This is easy because we are simply searching for a reference to the function label (R.pick, or pick in the import section of the project files).

The second, which perhaps we should have considered doing at the outset, is to create a facade for our library. In our utility function, we create delegate functions for the Ramda utilities we use and then we use our delegates throughout the project. Our pick function from our utils file delegates to R.pick. If we decide to move to a different library in the future, "correct" its behaviour, or hand-roll our own versions of these functions, we do so from a single location and our changes propagate to all use-cases.

As an added bonus, extracting utility work out of your main logic allows you to extract that logic right out of the file and into utility files, drastically cleaning up main-logic files. In the example just provided, Ramda provides pipe and merge, meaning they already exist outside of this hypothetical file. Our derivative pickWithDefaults can exist in our own utility file, meaning that only the defaultValues and final pickWithDefaults function execution line are actually in the final code--everything else can be imported. At the very least, utility functions can be moved to a portion of the file that seems appropriate. With function declarations (using the function keyword), the declaration can exist at the bottom of the file and be [hoisted](https://developer.mozilla.org/en-US/docs/Glossary/Hoisting) to the place of execution. Function expressions (using the arrow syntax), sadly, cannot be hoisted and need to be declared above the point of execution.

Without allowing it to overwhelm you, I encourage you to look at the documentation for the function utility library RamdaJS or Lodash. All that I wish to highlight is the abundance of utility functions that these libraries have identified as worthy of inclusion in your aresnal.

Unfortunately, Lodash has two variants--standard and "functional programming"/fp. The fp variant is necessary for what we are discussing here but the main documentation cannot be viewed in the "fp-style" of data-last function style. The biggest difference between the two versions is the order of parameters/arguments is such that the "data being acted on" comes last. The fp-guide does discuss other differences.

Another library that is thematically linked to this topic is RxJS, which is particularly useful for converting "events" (often real-world events) into data. This allows for the normalizing of event-handling in the manner just described. For example, "double-click" can be defined as a utility which is consumed throughout a project in a unified manner. Operators are functions which transform this data and eventually these transformed events are either consumed or ignored.

Conclusion

I genuinely believe that point-free style is a helpful in making my projects' main logic cleaner and more condensed. But this benefit comes at an expense or at least with some cautions.

If working with others who do not use point-free style, it can be jarring if done in excess. In several of the examples above, we created utility functions which omitted the data source (in order to avoid having to create a superfluous wrapping function).

const pickWithDefaults = (keys, defaults) => R.pipe(R.pick(keys), R.merge(defaults));

// Notice: Our data source is omitted, 
// which if included would be written as
const pickWithDefaults = (keys, defaults) => (object) => R.pipe(R.pick(keys), R.merge(defaults))(object);
Enter fullscreen mode Exit fullscreen mode

For your collegues' benefit, consider including the data source for documentation's sake. You would still gain the benefit of deploying it without needing to include it, and so it still has the desired impact.

Similarly, it is possible to chain a tremendous number of utilities together in a single block. There are even utility functions in libraries which replace the typical imperative operators, such as: if, ifElse, tryCatch, forEach, etc. Chaining together too many of these will result in your code looking pretty similar to a block of imperative code. Instead, try to think of functional blocks and define them such that they expose a simple interface. That way, chaining the pieces together documents your intent and reduces the chance that you get lost in your control flow.

While it can seem overwhelming at first, a utility library such as Ramda can be approach incrementally to great effect. Additionally, there are Typescript typings available for Ramda, though the README page does admit that there are certain limitations they have encountered in fully typing the library.

Lastly, as you split up your logic into utilities you are inherently creating abstractions. There is a popular addage within the coding community--AHA (avoid hasty abstractions). To an extent, this can be reduced by standing on the shoulders of existing library authors. The abstractions present libraries such as RamdaJS are not hasty, but rather longstanding ideas battle tested in the fields of functional programming and category theory. But in organizing our code, consider restraining yourself from writing code that doesn't come intuitively. Instead, write some code and then reflect on whether you see opportunities to clean it up. In time you will accumulate wisdom that will guide your future point-free efforts.

Discussion (0)

pic
Editor guide