Just Enough FP: Argument Order
In a previous post on currying, I used a filter
function in a way that may have left you scratching your head. Not because you were still learning currying, but for other reasons. Let me quickly write that function again for reference in this post:
const filter = predicate => array => array.filter(predicate)
Our curried filter function receives a predicate
argument (a predicate
is a function that returns a boolean) first, partially applies that to the new function returned, and then awaits an array
to operate upon. What might of had you scratching your head is the order of these arguments. Why did the predicate
come before the array
?
That’s a good question and the easiest way to find the answer is just to try it. Let’s write a badFilter
function with the argument order swapped:
const badFilter = array => predicate => array.filter(predicate)
Now, the array
is the first argument. This might seem more natural to many of you (and looks exactly like Lodash’s _.filter
method). With this order, your brain models it as, “I have an array, this is what I want to do to it.” But what’s the more reusable piece of that mental model? It’s not the data. The data can change at any time. It’s ephemeral, but what we want to do with our data, that is worth storing and reusing.
With the data argument first, we gain no advantage in partially applying it. We’d be better off just using the built in methods for that data type. And with the data first, we’ll never be able to pipe the result of another function into this one. As we’ll see in a future post on composition, having our data as the final argument is the key to being able to build up functional complexity with composition.
Let’s just let the bad example play out a bit:
const arr1 = [1, 2, 3, 4, 5]
const badFilterWithArr1 = badFilter(arr1) // returns a function awaiting a predicate
badFilterWithArr1(n => n % 2 === 0) // [2, 4]
badFilterWithArr1(n => n % 2 !== 0) // [1, 3, 5]
That’s no more useful than just calling the predicate functions on the the filter
method of the array.
arr1.filter(n => n % 2 === 0) // [2, 4]
The wrong argument order immediately negates any benefit we gained through currying and partial application, so it’s important that we have our data as our final argument in our functions.
Rules of Thumb for Argument Order
Here are my general rules for determining argument order. The first “rule”, as I just stated, is data comes last. You can bake in a lot of reusable functionality with partial application and pass new data into it over and over with this order.
But what if I have a function that doesn’t operate on data, but benefits from partial application and proper argument order?
In that case, the second “rule”, and this is really my way of thinking about it, is to order arguments from most stable to least stable argument. What does that mean?
In my previous post on partial application, I used the example of building up a function to fetch data from an API. That function looked like this:
const getFromAPI = baseURL => endpoint => callback =>
fetch(`${baseURL}${endpoint}`)
.then(res => res.json())
.then(data => callback(data))
.catch(err => {
console.log(err)
})
If we examine a REST APIs URLs (so many acronyms, sorry), the part of the URL that changes the least (and in a good API, never) is the baseURL
. If I want to fetch data from Github, I can make a partially applied function like this one:
const getFromGithub = getFromAPI('https://api.github.com')
This function is really useful! I can build up many endpoints from this one function.
const getUsersFromGithub = getFromGithub('/users')
const getReposFromGithub = getFromGithub('/repositories')
const getPublicGistsFromGithub = getFromGithub('/gists/public')
The endpoint
is less stable than the baseURL
, it changes frequently, but still less frequently than what we want to do with the data once we fetch it. We can do any number of things once we have our users. Thus, the callback
argument is last because it is the least stable of our arguments.
I’d give you a few examples, but literally you can do almost anything you want with the data once you have it. Use your imagination.
Conclusion
Argument order in curried functions makes a big difference in how useful and reusable a function is. It’s also paramount for enabling composition, as we will see in a future post.
In this series, I’m going over material from my Just Enough Functional Programming in JavaScript course on egghead. This post is based on the lesson Improve JavaScript Function Usability with Proper Argument Order in Functional Programming.