A Deeper Dive into Function Arity (With a Focus on Javascript)
Justin Morgan
Posted on December 21, 2020
If you are arriving at this article with any background in one or more of the popular programming languages of the day, you will most likely have at least an implicit understanding of what function arity
is. The term arity
refers simply to the number of parameters
in the definition of a function. This is casually expressed as how many arguments a function takes.
For many, this definition is sufficient. My goal is to convey a deeper understanding of this concept, and to tie it to other programming concepts which you may encounter (here, currying
and partial application
, but also point-free style
).
Arity Definitions
In programming circles where function arity is explicitly discussed, there is a set of related labels which are used to describe different kinds of function arity based on the number of arguments expected by a given function. They are:
-
Nullary
: zero arguments -
Unary
: one argument -
Binary
: two arguments -
Ternary
: three arguments -
N-ary
: havingN
arguments -
Variadic
: having a variable number of arguments
While it is possible that you may encounter specific names for a set of 4 or more arguments, it is uncommon. See the Wikipedia article on the topic for a more elaborate list of names available: Function arity.
Strict Arity Requirements
Some languages, especially those with a functional programming bent, will give more attention to the topic of arity than Javascript typically does. For example in the Elixir
language, you must provide precisely the number of arguments equal to the number of parameters in the function definition (except for those with provided default values). This requirement allows for a feature called multiple dispatch
, which is to say that a function identifier can have multiple definitions for different function arities (also based on different patterns of arguments provided):
# the `Elixir` notation used is the function_name/arity
# join_strings/2
def join_strings(list, combinator) do
Enum.join(list, combinator)
end
# join_strings/3
def join_strings(item1, item2, combinator) do
item1 <> combinator <> item2
end
iex> join_strings(["Cat", "Dog", "Ferret", "Monkey", "Parrot"], " & ")
"Cat & Dog & Ferret & Monkey & Parrot"
iex> join_strings("cat", "dog", " & ")
"cat & dog"
iex> join_strings("cat")
** (CompileError) iex: undefined function join_strings/1
iex> join_strings("cat", "dog", "parrot", "ferret", " & ")
** (CompileError) iex: undefined function join_strings/5
Contrast this with the design of the Haskell
programming language, where all functions are unary
(or nonary
/no-argument) functions. Here, it is ordinary that a function will be "partially applied
", returning another function rather than a "value" or "data".
-- The type signature reflects the unary nature of Haskell functions
add3 :: Number -> Number -> Number -> Number
add3 x y z = x + y + z
a = add3 10 -- `a` is a function y z = 10 + y + z
b = a 20 -- `b` is a function z = 10 + 20 + z
c = b 30 -- `c` is now evaluated to 60 (10 + 20 + 30)
But in Javascript, this requirement does not exist. In fact, functions can receive less or more than their "required" arguments and still proceed with execution. If fewer arguments are supplied than the function definition provides parameters for, then the "missing" arguments will be undefined
. If more arguments are passed than the definition provides parameters for, the declared and "extra" arguments are available via the reserved arguments
array-like object.
function logEmAll(a, b, c) {
console.log(`a: ${a}`)
console.log(`b: ${b}`)
console.log(`c: ${c}`)
for (let i = 0; i < arguments.length; i++) {
console.log(`arguments[${i}]: ${arguments[i]}`)
}
}
> logEmAll(1,2,3,4)
a: 1
b: 2
b: 3
arguments[0]: 1
arguments[1]: 2
arguments[2]: 3
arguments[3]: 4
We can see that if more arguments are passed than are required, the function continues with execution without issue. The "extra" arguments are merely not used (unless accessed via the arguments
object explicitly, which we have done in the above example).
In the Node framework, Express, this pattern is employed in the ubiquitous connect
-style callback throughout the framework. This results in "shifting" parameters depending on the context:
(request, response, next) => {...}
(request, response) => {...} // Omits the third `next` parameter when not used
(_request, response, next) => {...} // `_` marks the first parameter as not in use (idiom)
(error, request, response, next) => {...} // "Shifts" the parameters one position
(error, _request, _response, next) => {...} // "Shifts" the parameters one position and skips parameters
One characteristic demonstrated above is that the function definitions rely on positional arguments
. That is, the function consumes arguments based on their index in the arguments
list. To contrast this, there is an approach of named parameters/arguments
. For example, the Koa framework (created by the creators of Express), collapses the arguments of the equivalent callbacks into an object (the "context" object), which contains properties analogous to request
, response
, next
, and error
in the above examples.
With named arguments, the idea is that the function arguments are contained as properties on an object. We can mix the positional and named argument approaches, taking some positional arguments and a complex/object argument. This pattern is fairly common, whereby the final argument is an object of configuration options, allowing for the function to determine which options were or were not provided without cluttering up the function signature too much. But at its extreme, a function can be defined as taking one argument (a unary function
) that is an object containing multiple pieces of data to be consumed.
function userFactory(userTraits) {...}
// Taking advantage of ES2015 destructuring, the `named` quality is more apparent
function userFactory({name, email, address}){...}
One advantage to this approach is that the order of supplied arguments does not matter. Similarly, if arguments are omitted, the function signature and corresponding call are less noisy.
// Fictitious Express/Koa-like callbacks that support named arguments (mirroring above example)
({request, response}) => {...}
({next, request, response}) => {...} // Order swapped (for no reason)
({response, next}) => {...} // Request omitted
({request, response, next, error}) => {...} // No more "shifting" params
({error}) => {...} // Request, response, and next parameters omitted
Variadic Functions
That was a brief survey of common treatments of function arity in the Javascript community. But let us consider it differently for a moment. Another way is to think of all functions having a single argument (a unary function
) that:
- is an array (the
arguments
array-like object); and - is, as a convenience, destructured in the function signature.
When thought of in this way, we can gleam a better understanding into the idiom employed in ES2015+ whereby a function's arguments are "collected" using the "rest/spread" operator. This has become an increasingly common pattern for implementing variadic
functions.
// `pipe` will take any number of arguments (intended to be functions)
// and return a function which receives one argument that will be used
// as the input to the first argument, which will be the input to the
// second argument, which will be...etc
// pipe(f1, f2, f3)(value) --> f3(f2(f1(value)))
function pipe(...fns) {
return function(input) {
return fns.reduce((val, fn) => fn(val), input)
}
}
// Or like this, with the `fns` supplied as an array [fn1, fn2, fn3]
// pipe([f1, f2, f3])(value) --> f3(f2(f1(value)))
function pipe(fns) {
return function(input) {
return fns.reduce((val, fn) => fn(val), input)
}
}
// `pipe` could be rewritten as
// (highlighting the implicit unary-signature interpretation)
// pipe(f1, f2, f3)(value) --> f3(f2(f1(value)))
function pipe() {
// Before the inclusion of the rest/spread operator
// this would be accomplished with a loop acting
// on the `arguments` object
var [...fns] = arguments
return function(input) {
return fns.reduce((val, fn) => fn(val), input)
}
}
/*
The above is written as two unary functions to reflect a design common in the JS functional-programming community referred to as "data-last" signatures. This allows for a function to be "partially applied" and to be used in "pipelines" for greater compositional flexibility.
Additional information on this `data-last` signatures, `currying`, and `point-free style` are provided at the end.
*/
If you are unaware of this behaviour, and how to exploit it, you may find yourself writing more convoluted code than is necessary. For example, you may need to write utilities that behave like variadic functions
, but in failing to identify the ability to act on the arguments
object directly, you unnecessarily rewrite the same function to support multiple arities.
// `zip` is a common operation upon lists. Traditionally it takes one element from the
// head of each list and combines them into a new unit.
// ex. (2 lists) zip([1,2,3], ["a", "b", "c"]) --> [[1, "a"], [2, "b"], [3, "c"]]
// ex. (3 lists) zip([1,2,3], ["a", "b", "c"], ["!", "@", "#"]) --> [[1, "a", "!"], [2, "b", "@"], [3, "c", "#"]]
function zip2(list1, list2) {...}
function zip3(list1, list2, list3) {...}
function zip4(list1, list2, list3, list4) {...}
function zip(list1, list2, list3, list4) {
if (!list4 && !list3) { return zip2(list1, list2) }
else if (!list3) { return zip3(list1, list2, list3) }
else { return zip4(list1, list2, list3, list4) }
}
// Versus
function zip(...lists) { ... }
When you become aware of the nature of Javascript's treatment of arity, you open the door to learning more advanced coding patterns. Two such patterns, popular in the realm of functional programming and increasingly in the Javascript community generally, are partial application
and the related concept of currying
. These two patterns heavily employ and exploit knowledge of function-arity.
Currying vs Partial Application
When observing currying
and partial application
in effect, people often. collapse their understanding of one into the other. I believe that part of this misunderstanding stems from the a prevalent notion that functions are not "real values". Put another way, that a function which returns a function "isn't really done yet".
An example. Let's say that we have a collection of users and a function which takes an options argument which describes the behaviour that the filter function will operate.
function filter_users(filter_options, users_collection) { ... }
We may want to particularize this function into a number of other functions.
const filter_params_without_email = {...}
const filter_users_without_emails = filter_users.bind(null, filter_params_without_email)
.bind()
is a native Javascript method that all functions "inherit" that:
- returns a new function that is a copy of the attached function (here
filter_users
); - assigns a value to the
this
keyword in the execution context of the new function (unused in this example); and - "partially applies" arguments to the function when it is called.
In some languages, the bind
method would be unnecessary. You would instead call the function with the arguments you have available, they are applied positionally according to whatever rule of the language in question sets, and you get a function in return that is awaiting just the remaining positional arguments.
The point of misunderstanding is in the notation of how Javascript (and many other popular languages) implement functions. As we described above, a Javascript function can be thought of as being a unary function
which is provided its argument in an array (technically, an array-like object). And by the syntactic sugar of the language, these arguments have been destructured so as to ease their access within the function body. It would be a similar situation if we adopted the named argument
approach using an object rather than an array to store our arguments. Upon receiving it's one and only argument set (positional or named arguments), it attempts to access the specific indices/properties of this argument set immediately. If these are not all provided, you may encounter property access errors for those missing arguments.
What bind is doing is holding onto those initially supplied arguments, holding onto a reference to the original function, and returning a new function for you to use with a remapping of arguments (i.e. the "second" positional argument becomes the "first" positional argument in the new function).
Currying on the other-hand, introduces a different premise. Currying is the whole-hearted embrace of unary
(and nullary
/no-argument) functions. To "curry
a function" is to define it as such that it accepts one argument and
returns either a function or a value. It is possible to curry
a function that was not initially defined in such a manner, using the .bind()
method described
above or a utility such as the ones provided in the several functional programming
libraries (some of which are listed at the end).
A toy example would be addition. A non-curried implementation of addition could look like:
function add(a, b) {
return a + b
}
To curry
this function would be to define it as such:
function add(a) {
return function (b) {
return a + b
}
}
Well that's terrible. Why would we do that? As of ES2015, there is an alternative syntax (with its own quirks, to be sure) for more succinctly representing currying (with arrow function expressions).
const add = (a) => (b) => a + b
Ooh, that's even cleaner than the original. If you'd like to know more about ES2015 "arrow function expressions" you can follow this link to the MDN Web Docs.
What's more is that this silly example can be particularized
very easily.
const add2 = add(2) // add2 is now a function // add2(4) --> 6
const add3 = add(3) // add3 is now a function // add3(4) --> 7
To return to the earlier "partial application" example, now curried:
const filter_users = (filter_options) => (users_collection) => { ... }
// filter_users_without_emails will be a fn awaiting data
const filter_users_without_emails = filter_users({...filter_params})
To explain what is happening, it should be highlighted that returning a new function from a function is often very useful. It should not be thought of as a "mid-way point" in execution. By using currying and "partially applied" functions, you can drastically clean up your code.
For example, using the pipe
function described above, one can destructure a code block into single purpose functions and then compose them back together, with the function descriptors serving as documentation.
// These functions can be generalized and/or perhaps imported from a utility file
const asyncFunctionReturnsPromiseOfUser = (req) => {...}
const getPostsFromUser = (sortOrder = "desc") => ({id}) {...}
const excludeOlderThan = (oldestDate = "1970-01-01") => (posts) {...}
const includeOnlyWithTags = (tags) => posts => {...}
const getUsersPostsCallback = (req, res) => {
// `pipe` (and therefore `filterPosts`) returns a function which awaits data,
// in this case a list of posts (`data-last` and `point-free` styles)
const filterPosts = pipe(
excludeOlderThan(req.params.oldest),
includeOnlyWithTags(req.params.tags)
)
asyncFunctionReturnsPromiseOfUser
.then(getPostsFromUser("asc"))
// `then` has an implicit unary callback with the data from the resolved promise
// i.e. (user) => {...}
// `getPostsFromUser("asc") returns a unary function expecting a user
// and is provided as the callback to `then`
// equivalently written as `(user) => getPostsFromuser("asc")(user)`
.then(filterPosts)
}
If you are interested in exploring the claimed advantages of currying, I recommend exploring the following topics:
- Why Curry Helps
- Favoring Curry
- Data-Last Function Signatures
- Point-free Style
- Lamda Calculus (Stanford Encyclopedia of
Philosophy) - Functional Programming Libraries
- Compile to Javascript languages which embrace functional programming and currying
Posted on December 21, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.