April 29, 2019

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.

Categories
JavaScriptWeb DevelopmentFunctional Programming

+0

Liked the post? Why not show it?! Stroke Kyle's ego by stroking clicking his beard. You can click up to 50 times if you really liked it.


Related Posts:
Spot a typo? Submit a PR with the fix! This entire blog is open sourced at https://github.com/kyleshevlin/blog
Newer Post: Just Enough FP: Pointfree
Kyle Shevlin's face, which is mostly a beard with eyes
Kyle Shevlin is a front end web developer and software engineer who specializes in JavaScript and React.