Div’s Blog

December 01, 20197 min read

Function arity and partial application

Partial application is an interesting functional programming technique to prepare (and simplify) a function for future use. More specifically, its a delegation technique that lets us create specialized functions from generalized ones.

In my previous blog post on functional programming constructs I wrote about the principles of functional programming and how we adopt them in JavaScript. In this one, I’ll continue to expand on those into partial application.

Arity

Arity is the number of parameters a function expects. eg:

function sum(a, b) {
	return a + b;
}

console.log(sum.length); // 2 - arity of sum function

There is a slight difference between the terms arguments and parameters. Arguments are the values that are passed to a function whereas, parameters are the variables in function definition which receives those values as inputs.

The above example of using the function’s length property doesn’t work always. Here are a few quirks:

function sum1(a, b = 1) {
	return a + b;
}
console.log(sum1.length); // 1 - ignores default parameter

function sumAll(a, ...rest) {
	// sums all values
}
console.log(sumAll.length); // 1 - ignores rest parameter

Therefore, for such functions we’ve to know the arity of the function beforehand in order to use some of the functional programming techniques.

Generally, functional programming avoids using functions that can accept an indeterminate number of arguments (also called variadic functions).

Working with function inputs

Sometimes reducing the surface area of function inputs can be quite helpful in adopting functional approaches. For example, consider a unary utility (also available in functional libraries such as RamdaJS).

unary

unary is a higher order function which reduces the arity of a provided function to 1. Similarly, there may be any n-ary function to reduce the arity of a function to n.

const unary = fn => arg => fn(arg);

This may be used in certain situations, for example consider if we were to take an array of serialized numbers and return a parsed array of numbers.

// Convert ['1', '2', '3'] to [1, 2, 3]

// Some libraries have this and many other functional utilities
const unary = fn => arg => fn(arg);

const data = ['1', '2', '3'];

console.log(data.map(parseInt)); // [1, NaN, NaN]

console.log(data.map(unary(parseInt))); // [1, 2, 3]

Why does this happen? The reason is map invokes the callback (in the first case parseInt) with three parameters - current array value, index and the entire array. Since parseInt has an arity of 2 (accepts the string value and radix) the evaluation happens as follows:

// Mapping over the array ['1', '2', '3']
// Here's a simplified version of what happens
// data.map((item, index) => parseInt(item, index))

parseInt('1', 0); // 1
parseInt('2', 1); // NaN
parseInt('3', 2); // NaN

// And here's how the second reduced arity example works
// a unary variant of parseInt would accept only one argument
// data.map((item, index) => parseInt(item))

parseInt('1'); // 1
parseInt('2'); // 2
parseInt('3'); // 3

Note

Although I’ve used parseInt example to showcase how reducing arity can be helpful, it’s always best to explicitly pass the radix parameter as 10 since it doesn’t default to 10.

identity

An identity function take an input and returns it as it is:

const identity = n => n;

By itself, it may not look much useful but consider an example where we want to filter out falsy values:

const identity = n => n;

const data = ['a', '', null, undefined, 'b'];

console.log(data.filter(identity)); // ['a', 'b']

This works because of coercion.

identity function can also be used as a default transformation function. Consider an arbitrary example:

const add1 = n => n + 1;
const identity = n => n;

// Returns a function that either increments the value or does nothing
const addIfPositive = n => (n > 0 ? add1 : identity);

console.log(addIfPositive(1)(7)); // 8
console.log(addIfPositive(-1)(7)); // 7

Delegating

Delegation is a technique in which we only do the minimal required work upfront and leave the rest for later (if and when it’s required).

This is often called lazy evaluation with respect to functions. When delegating a function, we can specify only some inputs initially and let the function accept the remaining inputs later when needed.

With delegation techniques, we can transform a generalized function to more specialized form (by reducing the surface area of function’s inputs). A couple of ways to do this are partial application and currying.

Partial application

A couple of common functional utilities are partial and partialRight:

const partial = (fn, ...presets) => (...args) => fn(...presets, ...args);

// partialRight can be used where we want to pass the rightmost arg first
const partialRight = (fn, ...presets) => (...args) => fn(...args, presets);

// An example of partial application
const data = {
	bands: {
		oasis: 'inactive',
		beatles: 'inactive',
		gnr: 'active',
	},
};

// A generalized function
const getValue = (obj, key) => obj[key];

// A specialized function
const getBand = partial(getValue, data.bands);

console.log(getBand('oasis')); // 'inactive'
console.log(getBand('beatles')); // 'inactive'

Currying

The term currying has its origin from the mathematician Haskell Curry. This technique is used extensively in purely functional languages such as haskell.

In Haskell, all functions are considered curried: That is, all functions in Haskell take just one argument.

Currying transforms a function that expects multiple arguments into a chain of functions, each accepting a single argument and returning another function to accept the next argument and so on. An example would make this explanation clearer:

const sum = (a, b, c) => a + b + c;

const sumCurried = a => b => c => sum(a, b, c);

console.log(sum(1, 2, 3)); // 6
console.log(sumCurried(1)(2)(3)); // 6

In the above example, both sum and sumCurried serves the same final purpose, to provide a sum of three values. However, the way of achieving that purpose is different. sumCurried is a curried version of the sum function which was formed by reducing (or unwinding) the arity of the original sum function from 3 to 1 (as a chain of unary functions).

Currying, like partial application also transforms a generalized function to a more specialized one. A key difference between partial application and currying is that partial needs to pass all partially applied arguments initially. With currying we can pass subsequent arguments lazily.

const sumCurried = a => b => c => a + b + c;

const add1 = sumCurried(1);
const add2 = add1(2);
const add3 = add2(3);

console.log(add3); // 6

Both currying and partial application use closures.

You can find utilities for curry in most functional libraries (such as RamdaJS). But here’s how you may implement it on your own:

function curry(fn, arity = fn.length) {
	return function curriedFn(...args) {
		if (args.length >= arity) {
			// invoke the original function
			return fn(...args);
		}
		return (...nextArgs) => curriedFn(...args, ...nextArgs);
	};
}

const sum = (a, b, c) => a + b + c;

console.log(curry(sum)(1)(2)(3)); // 6

// Generally we keep curried functions unary but the following also work regardless
console.log(curry(sum)(1)(2, 3)); // 6
console.log(curry(sum)(1, 2, 3)); // 6

Uncurrying

Uncurrying transforms a curried function back to its original form.

// transforms sum(1)(2)(3) to sum(1, 2, 3) form
const uncurry = fn => (...args) =>
	args.reduce((nextFn, arg) => nextFn(arg), fn);

const curriedSum = a => b => c => a + b + c;
const sum = uncurry(curriedSum);

console.log(sum(1, 2, 3)); // 6

Further reading

This is the second post I wrote about functional programming. You may check out the first post which also includes links to free resources. I’ll continue to share my learnings on functional programming in future posts.


Divyanshu Maithani

Personal blog of Divyanshu Maithani. I’m a JavaScript engineer working with React, React Native, GraphQL and Node. I also create programming videos with my friend. In my spare time I play music and DoTA.

You may follow me on twitter or join my newsletter for latest updates.