April 29, 2019

Just Enough FP: Argument Order

edit

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.


Liked the post?
Give the author a dopamine boost with a few "beard strokes". Click the beard up to 50 times to show your appreciation.
Kyle Shevlin's face, which is mostly a beard with eyes

Kyle Shevlin is the founder & lead software engineer of Agathist, a software development firm with a mission to build good software with good people.

Logo for Introduction to State Machines and XState
Introduction to State Machines and XState
Check out my courses!
If you enjoy my posts, you might enjoy my courses, too. Click the button to view the course or go to Courses for more information.
Sign up for my newsletter
Let's chat some more about TypeScript, React, and frontend web development. Unsubscribe at any time.