DEV Community

Cover image for Self-Documenting Function Calls
Adam Nathaniel Davis
Adam Nathaniel Davis

Posted on • Edited on

Self-Documenting Function Calls

There are a few languages I've used that have named parameters. Named parameters can be pretty cool because:

  1. They allow you to pass the parameters into the function in any order.

  2. They foster self-documenting code because, at the point where the function is called, the parameter values and the names of those parameters are spelled out right there in the code.

Alas, JavaScript (my most-favoritest coding language ever - until I find a new most-favoritest coding language ever...) doesn't have named parameters. In fact, most languages don't. But that's OK. There are still ways that we can write some awesome self-documenting code without named parameters.

Let's imagine that we have this function:

const searchRecipes = (
   searchText = ''
   ,includeSpicy = true
   ,onlyVegan = false
   ,includeSponsoredResults = true
) => {
   // do all the recipe-search magic here
};
Enter fullscreen mode Exit fullscreen mode

We have a pretty good start to a clear, concise function. The name of the function seems self-explanatory. The parameters are given clear names that aren't lazily abbreviated (e.g., no srchTxt, inclSpcy, onlyVeg, or inclSpnsRes).

We also have default values on all of the parameters. This helps us write cleaner logic - because we can assume a default value if none was provided. It also helps our IDE to suss out potential code smells (like, for example, if we tried to pass an object as the searchText argument).

Losing the Context When the Function is Called

If there's any "problem" with this function, it's not in how it's defined. It's in how it's called. Unfortunately, JavaScript doesn't (by default) give us the tools to control how a function is called. We can only really control how it's declared. Once we've declared it and it's out there, "in the wild", for any devs on your team to use, it can possibly be called in any ol' haphazard way.

Imagine that, somewhere else in our code, we need to invoke this function. So we do it like this:

searchRecipes('linguine', true, false, false);
Enter fullscreen mode Exit fullscreen mode

Suddenly, all of the glorious context in the function declaration has flown out the window. Any casual onlooker who reads this line of code can probably guess that we're searching for recipes, and that we specifically want recipes that have something to do with "linguine".

But... true, false, false?? What the hell does that mean? And what happens if we start flipping those Booleans? There's no way to tell by looking at the function invocation.

(Side note: You may be thinking, "Well, in my Super Cool IDE, the names of those variables are displayed for me - either statically, or when I hover over the function call." To which I'd say, "Yeah, that's great. I have a Super Cool IDE as well. But well-written code isn't dependent upon whether other coders are reading it through the same IDE that you are, with the same settings that you're using.")

Dragging the Context (Kicking-and-Screaming) Back Into the Function Call

You may think that there's not much we can do about this. After all, the function accepts a string followed by three Booleans. And that's exactly what we provided to it. We can't help it if those values have no implicit context, right?

Well, no, not exactly. We do have some significant control over how we call the function. The technique I'm about to illustrate is incredibly simple and easy to use. And yet, I rarely-if-ever see this in the codebases I'm exposed to. I'm talking about defining inline variables in the function call, like so:

searchRecipes(
   searchText = 'linguine'
   ,includeSpicy = true
   ,onlyVegan = false
   ,includeSponsoredResults = false
);
Enter fullscreen mode Exit fullscreen mode

The invocation above does the exact same thing as searchRecipes('linguine', true, false, false) but this approach takes all the mystery out of those Booleans. We now know, just by reading the function call, that we're searching for linguine recipes, that may be spicy, that are not restricted to vegan dishes, and we don't want any sponsored results.

There is a bit of a problem with this approach, however. You sharp-eyed JS hawks will probably notice that I'm assigning values to undeclared variables. You can do that in JS code that is not running in "strict mode". But if you're using "strict mode" (and you absolutely should be using "strict mode"), the previous example won't compile.

Most modern frameworks - like React - are automatically running in "strict mode". So whether you realize it or not, you're probably writing-and-deploying your code in strict mode.

In strict mode, we must first declare our variables - with a const or a let - before using them. So what if we try it like this?

searchRecipes(
   const searchText = 'linquine'
   ,const includeSpicy = true
   ,const onlyVegan = false
   ,const includeSponsoredResults = false
);
Enter fullscreen mode Exit fullscreen mode

Umm... no. Sorry. That doesn't compile either.

So are we back to calling this as searchRecipes('linguine', true, false, false)? No. We have other options. The most obvious one is to have those variables declared before we get to the function call. That would look like this:

const searchText = 'linguine';
const includeSpicy = true;
const onlyVegan = false;
const includeSponsoredResults = false;
searchRecipes(searchText, includeSpicy, onlyVegan, includeSponsoredResults);
Enter fullscreen mode Exit fullscreen mode

OK, we've satisfied the "strict mode" compiler. We've preserved all the valuable context that goes along with those argument names. However, I will admit that this feels like a lot of extra variable definitions to load into the file.

Obviously, this approach works wonderfully if you already had those variables defined somewhere else in the previous instructions. But this might feel to many as too much "overhead" if you're only defining all of these variables just for the sake of readability.

Another way to achieve this effect, while still minimizing the overhead of purely-documentary code, is to define these values in a global constants file. You may not have such a file. You may not even want such a file. But if you do, it would look something like this:

// constants.js
const constants = {
   includeSpicy : true,
   doNotIncludeSpicy : false,
   limitToVegan : true,
   doNotLimitToVegan : false,
   includeSponsoredResults : true,
   excludeSponsoredResults : false,
}
export default constants;
Enter fullscreen mode Exit fullscreen mode
// constants was imported above
searchRecipes(
   'linguine'
   ,constants.includeSpicy
   ,constants.doNotLimitToVegan
   ,constants.excludeSponsoredResults
);
Enter fullscreen mode Exit fullscreen mode

Notice that, to make the code optimally-readable, we defined two potential variables for each of the Boolean values that must be passed in. One for the true condition and one for the false.

Of course, there are still trade-offs in this approach. It implies the importation of a common constants file. It still requires that the variables be stored somewhere. Also, it can make the logic a bit difficult to follow (notice that to exlude spicy dishes, we're using a constants.doNotIncludeSpicy variable that evaluates to false). So if even this approach doesn't suit you, we'd still have to look for other options.

Thankfully, there's still one more method that satisfies the "strict mode" compiler and requires very few extra lines of code.

Object Magic

JavaScript, in "strict mode", requires us to declare our variables before using them. But objects provide a sort of backdoor workaround to this. The object itself must be declared. But we can define properties on that object on the fly. That's useful in our present example because now we can do something like this:

let arg = {};
searchRecipes(
   arg.searchText = 'linguine'
   ,arg.includeSpicy = true
   ,arg.onlyVegan = false
   ,arg.includeSponsoredResults = false
);
Enter fullscreen mode Exit fullscreen mode

So all we had to do was add one extra line of code above the function call to define a dummy object that will serve as a bucket for any of our inline argument definitions. And the nice thing is that if you have 50 more function calls that are similar to this (in the same scope), you can reuse that arg object as often as you like.

Now we have a function call that's fully self-documenting. And it complies with ES6's "strict mode" requirements.

When Do We Need This??

I'm not advocating to use this approach on every damn function call. In fact, it probably doesn't need to be used on most function calls. Consider the following example:

const searchResults = searchRecipes(userSuppliedSearchString);
Enter fullscreen mode Exit fullscreen mode

We know from the original function definition that the includeSpicy, onlyVegan, and includeSponsoredResults arguments are optional. Given the name of the function, and the name of the variable that's passed into the first argument, it's pretty clear what's happening here. I don't need anyone to explain to me that we're searching for recipes based on a user-supplied value. So in this case, it's probably overkill to explicitly name the argument as we pass it into the function.

In fact, most function calls that pass in only a single argument should be fairly self-explanatory. And the readability factor is further enhanced when our arguments are already stored in descriptively-named variables.

The most common use-case for this approach is when you have a function that has multiple arguments - especially when many of those arguments are switches/flags/enums that don't make any intuitive sense on the calling end.

This is especially true of built-in language functions that may require an obtuse value (like a Boolean) that is almost-never self-explanatory when looking at the invocation. That's why I used Booleans in the example - because Boolean values, passed into a function, almost never make intuitive, easily-readable "sense" when you are simply perusing them from the calling end.

Top comments (5)

Collapse
 
jorensm profile image
JorensM

Thanks for the great article! I know it's a bit dated but I'd like to share my points anyway:

  1. You could just do !includeSpicy instead of doNotIncludeSpicy

  2. The most common and IMO the most effective/readable/efficient way is to declare the function argument as an object, then you can pass an object to the function call and clearly see all the property names. This is the most common way nowadays when you have a lot of args in a function.

  3. Take a look into the builder pattern. It's usually used in classes but can be applied to functions as well.

But anyway, thanks for the great article! Readability is an important aspect of coding and I appreciate you writing about it!

Collapse
 
devdufutur profile image
Rudy Nappée • Edited

In JS, maybe you could've use named parameters with object destructuring like that :

// definition
const searchRecipes = ({
   searchText = "",
   includeSpicy = true,
   onlyVegan = false,
   includeSponsoredResults = true
}) => {
   // do all the recipe-search magic here
};

// Call
searchRecipes({
   searchText: "linguine",
   includeSpicy: true,
   onlyVegan: false,
   includeSponsoredResults: true
}) 
Enter fullscreen mode Exit fullscreen mode
Collapse
 
pclundaahl profile image
Patrick Charles-Lundaahl • Edited

Something I tend to do on personal projects: when I'm writing functions (or constructors) that takes more than a couple of arguments, I tend to bundle all of the arguments into a single object. This way, I force users to explicitly state the name of the arguments they're passing in.

Granted, this works a lot better in TypeScript, where the compiler yells at you if you don't supply required args.

Also, bravo: I've never seen that approach before. I'm not sure I like it, but it's damned cool that you can do that!

Collapse
 
bytebodger profile image
Adam Nathaniel Davis

Yeah, I was originally intending to include object-wrapped arguments in this post. But then I really thought that I wanted to make that its own future post, cuz it can be really useful - but there are at least some potential downsides.

I wanted to write this particular post from the perspective of how to call a function in this manner when you either don't have access to write/define/clarify the function itself, or when it's just not practical for you to do so.

Also, there are many built-in functions, or core-library functions, that you basically have no option except to call them as-is. So, in those cases, there are still ways to call them such that future readers can clearly see what's being passed in.

Collapse
 
pclundaahl profile image
Patrick Charles-Lundaahl

Ah! That makes a lot of sense. Thanks!