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.
Personal blog of Divyanshu Maithani. I’m a software engineer working mostly on frontend. 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.