On the ordering of function arguments
A lot of people think I nitpick when I talk about the order of arguments in a function. Let's try to put an end to that!
People who know me are probably tired of me bringing this up by now, but I can't stress enough the importance of ordering of arguments to a function. I recently came across a video which sparked the idea of writing about it just so I can have some place I can send people to if I need to talk about this idea again.
I mentor a few people on the amazing Exercism platform when I can find the time, and this year they have a new challenge called 48 in 24. It's about solving a problem every week in 3 different languages, (48 problems, one each week, in 2024, hence the name). I saw one of their first videos which was about solving the problem Leap
which is quite simple, and several approaches were discussed. I'd really encourage you to participate in the challenge, and also to spend time on the platform, contributing in whatever way you can. Here's the video in case you need to watch:
A simple example
In particular, on the clojure section, we have this code:
(ns leap)
(defn divisible-by? [x y]
(zero? (rem x y)))
(defn leap-year? [year]
(and (divisible-by? year 4)
(or (not (divisible-by? year 100))
(divisible-by? year 400))))
For the benefit of people that aren't familiar with clojure
, I want to take this into JavaScript. If we just converted that to JavaScript, we'd have:
const isDivisibleBy = (x, y) =>
x % y === 0;
const isLeapYear = (year) =>
isDivisibleBy(year, 4) &&
(!isDivisibleBy(year, 100) || (isDivisibleBy(year, 400))
If we wrote operators also as functions, we'd have:
const isZero = (x) => x === 0;
const remainder = (x, y) => x % y;
const isDivisibleBy = (x, y) =>
isZero(remainder(x, y));
const and = (...args) => args.reduce((x, y) => x && y, true);
const or = (...args) => args.reduce((x, y) => x || y, false);
const not = (x) => !x;
const isLeapYear = (year) =>
and(isDivisibleBy(year, 4),
or(not(isDivisibleBy(year, 100)),
isDivisibleBy(year, 400)))
Assuming we have isZero
, remainder
, and
, or
, and not
already defined somewhere, we could just write:
const isDivisibleBy = (x, y) =>
isZero(remainder(x, y));
const isLeapYear = (year) =>
and(isDivisibleBy(year, 4),
or(not(isDivisibleBy(year, 100)),
isDivisibleBy(year, 400)))
And it looks very similar to the clojure
version above.
Does the ordering matter?
Most of the time when dealing with a function that takes more than one argument (or a curried function as we'll see later), yes. To be more specific, ordering matters when arguments are partially applied to a function. Partial application is when some of the arguments to a function are supplied, and a new function is created which waits on the remaining arguments.
Let's take a simpler example:
const add = (x, y) => x + y;
This is a simple function that adds 2 numbers. Imagine we want to create 2 other functions inc
and dec
which are for incrementing and decrementing a number:
const inc = (y) => 1 + y;
const dec = (y) => -1 + y;
If we can see that these are very similar to add, and we just used x = 1
and x = -1
we could re-use the add
function, and partially apply some parameters to create those functions:
const inc = add.bind(null, 1);
const dec = add.bind(null, -1);
Function.prototype.bind
is used to bind function arguments to a function, resulting in a new function with the bindings. It's important to note that binding works on the order of arguments to a function, and the first parameter to bind is always the implicit this
argument of the function. In the case of a function that does not belong to a prototype chain (that is not part of a class hierarchy) like our add
function, we can safely bind the this
parameter to null
, as there's no use of this
in the function definition.
Now if we call inc
or dec
they will both work as expected:
console.log(inc(1)); // 2
console.log(dec(2)); // 1
It's important to note that bind
works with any number of arguments, and also creates a new function even if all the expected arguments are supplied - this might come in handy at times!
const three = add.bind(null, 1, 2);
const alsoThree = inc.bind(null, 2);
const threeAsWell = dec.bind(null, 4);
const stillthree = three.bind(null, 4);
console.log(three()); // 3
console.log(alsoThree()); // 3
console.log(threeAsWell()); // 3
console.log(stillThree()); // 3
Now that we have gotten bind
and partial application out of the way, let's get back to our isDivisibleBy
definition.
Order of arguments for isDivisibleBy
Like increment and decrement, what additional helper functions do we want to create by partially applying the arguments to our isDivisibleBy
function? In our case, we want functions like isDivisibleBy4
, isDivisibleBy100
and isDivisibleBy400
. However, given the first argument to this function is not the divisor
, it's hard to create those naturally:
const isDivisibleBy = (x, y) =>
isZero(remainder(x, y));
const isDivisibleBy4 = (x) => isDivisibleBy(x, 4);
const isDivisibleBy100 = (x) => isDivisibleBy(x, 100);
const isDivisibleBy400 = (x) => isDivisibleBy(x, 400);
These are perfectly valid definitions! However, if we defined isDivisibleBy
with the order of the arguments reversed, it becomes easier and simpler to understand:
const isDivisibleBy = (divisor, x) =>
isZero(remainder(x, divisor);
const isDivisibleBy4 = isDivisibleBy.bind(null, 4);
const isDivisibleBy100 = isDivisibleBy.bind(null, 100);
const isDivisibleBy400 = isDivisibleBy.bind(null, 400);
What we saw above works all well and good, but that was for this particular use case. But is there really a one-size-fits-all? What is the ideal order of arguments for a function?
Ideal ordering
A simple rule to follow is to think about which arguments are more useful to be dynamic, and keep the order as the least dynamic to the most dynamic in order to be useful. Let's take a few examples to see how that might work:
Testing for divisibility
In our isDivisibleBy
example, the function becomes more useful as we fix the divisor. As with a fixed divisor, we can test several other values of x for the divisibility. So the function is more helpful with the divisor as the first argument, as it can be partially applied.
const isDivisibleBy = (divisor, x) =>
isZero(remainder(x, divisor);
const isEven = isDivisibleBy.bind(null, 2);
const isDivisibleBy4 = isDivisibleBy.bind(null, 4);
Sorting a list using a compare
Let's take another example where we want a sort function that takes a list/array and a compare function. What's the right order of arguments? Let's think about the way that the function will be used. It's likely that we want to sort several arrays the same way - but there may be a few ways to compare that will be reused across them:
const sort = (compare, arr) => arr.sort(compare);
const sortByName = sort.bind(null, x => x.name);
const sortByAge = sort.bind(null, x => x.age);
const sorted = sortByName(playersFromApi); // example usage
Setting a value at a particular key in a map
Given we've looked at functions with 2 arguments, let's extend it to 3: setting a value at a particular key in a map. I understand it's more complicated, but give it a try - this is your first exercise. When you feel like you've got the answer, read on.
It's again helpful to think about it in terms of a use case, to think about less dynamic to more dynamic. Let's take an example of a lookup of an account id to account information. It's more helpful to be able to set multiple different values (at different times) at a single key, than it is to set a single value at multiple keys. It only follows that the value is more dynamic than the key. But what about the map itself?
A good rule of thumb is to that the data structure under manipulation is always better suited to be the last, in the case of examples like map and list. But there's a reason for this. It's the data structure itself that is always the most dynamic, as evidenced by our previous example. To think of it another way, whenever we set a key value pair in a map, we update the map (which is a different map logically). So given whenever a map is updated it's a different map, we want to apply a single key value pair on a different map every time (the latest one with the latest changes). Hence the map is the most dynamic (it can have different values at different points in time):
const set = (key, value, map) => map.set(key, value);
const setZero = set.bind(null, '0');
const setZeroA = setZero.bind(null, 'a')
// usage
const map = setZeroA(new Map());
console.log(map.get('0')) // 'a'
const other = setZero('b', map);
console.log(other.get('0')) // 'b'
// javascript maps aren't immutable, so map.get('0') is also 'b'
Exercise Time!
Usually, by thinking a little about the function and its applications, we can sense which order is more useful. A good exercise is to keep thinking about the order of arguments for every function you come across and try to evaluate if a different ordering would make it more useful.
- Imagine a
get
function for a map: it needs at least 2 arguments (the map and the key), to return the value at that key, so what's the best order you'd suggest and why? - What if we wanted a
defaultValue
to be returned in case the map doesn't have the key? Imagine we call this functiongetOr
: what's the best order you can think of and why? - Which of these functions can be used to create the other?
Order of arguments in curried functions
There are curried functions where instead of taking a list of arguments, a function takes one argument at a time. Curried functions are more composable, and require much less work to partially apply. As an example, our add
could've been defined as:
const add = x => y => x + y;
That makes it easier to define inc
and dec
in its terms:
const inc = add(1);
const dec = add(-1);
While this is very useful, it's not required to benefit from the ordering of arguments in the case of partial application. Curried or not, the ordering matters.
Changing the order
Sometimes, we may need multiple different ways to order functions. Or, we may not be the authors of the functions. In cases where we need to change the order of arguments to make it more useful, it helps to create such new functions with the right order for your use case. Here's a simple flip
function I use a lot, which is useful when the ordering is exactly the reverse:
const flip = f => (...args) => f(...args.reverse());
As an example, with our original definition or isDivisibleBy
we can still create a flipped version for partial application:
const isDivisibleBy = (x, y) =>
isZero(remainder(x, y));
const isDivisibleByN = flip(isDivisibleBy);
const isDivisibleBy4 = isDivisibleByN.bind(null, 4);
const isDivisibleBy100 = isDivisibleByN.bind(null, 100);
const isDivisibleBy400 = isDivisibleByN.bind(null, 400);
If you'd like to change the order of arguments more specifically, just define your own function that calls in to the other:
// example, to compute y = mx + c
const f = (m, c, x) => m * x + c;
// more useful
const y = (m, x, c) => f(m, c, x);
Costs
Note that in some languages and run-times (like JavaScript), partial application and currying incurs a performance cost. Calling through multiple functions to achieve the same result as calling a single function with different arguments is definitely slower. However abstractions always come at a cost, while enhancing flexibility, maintainability, and perhaps even security. It's important to consider your use case and weigh the costs with the benefits and decide accordingly.
Conclusion
I hope that was useful. I'd recommend the exercise of checking the order of arguments to a function with regards to utility in terms of partial application and/or currying. Hopefully we all write more useful functions!