Main | All posts | Code

Code Complete: Good and Bad Practices When Designing Function Parameters

Time Reading ~6 minutes
Code Complete: Good and Bad Practices When Designing Function Parameters main picture

In this article, I'll look at bad function design decisions that don't seem too bad at first glance. Optional parameters in JavaScript, flag handling, violation interfaces, and the misuse of the rest operator.

Some of the examples below are specific to JavaScript, while the others are universal

Required and optional parameters

JavaScript, unlike most other languages, does not check for required parameters when calling functions. Any function can be called without arguments, and the interpreter would just execute it substituting undefined into all the required parameters:


// The value is required but we didn't provide it. Undefined occurred inside
// The square root of undefined is NaN, which means that it's not a number
Math.sqrt(); // NaN

// An error occurs here, not when calling but when executing the map function
// Since the callback is not provided, it becomes undefined
[].map();
// Uncaught TypeError: undefined is not a function
// at Array.map (<anonymous>)

In most cases, such behavior is undesirable. It should rather be regarded as a legacy code from which we cannot escape. And it's certainly a bad idea to use it in your own code. Why?

Such function behavior leads to ambiguity when working with optional parameters. Imagine a function that takes Markdown as input and outputs HTML. Typically, the second parameter of this function accepts various behavior settings, such as tag escaping:

text = '*text*';
markdown(text); // '<b>text</b>'

// sanitize removes potentially harmful tags
markdown(text, { sanitize: true });

There are several ways to define this function. One of them is: const markdown = (text, options) =>. In this case, if the options are not provided, then the value of the options parameter becomes undefined. The necessary checks to work with these options build.

Although such code works and doesn’t seem scary, it’s a code that is difficult to analyze. It is not enough for a developer who needs to use this library to look at the definition of this function (for example, at the code snippet). Parameter optionality isn’t obvious from this definition. It must be explicitly set in the documentation, which means that the ability to generate documentation automatically disappears. In addition, it’s not clear what the options parameter is. Such an approach is inconvenient even from the standpoint of writing a function. Because options can change the type (object or undefined), we will need an additional check inside to see what we are working with. If it would always be an object, no check would be necessary. The correct definition is as follows:

const markdown = (text, options = {}) => {
  // ...
}

... usage

The rest syntax in JavaScript allows you to convert values into arrays. It's also useful in functions with a variable number of similar parameters. For example, the built-in function Math.min() takes any number of arguments and finds the lowest value:

Math.min(); // What will return this call?)
Math.min(1, 10); // 1
Math.min(2, -3, 1, 10) // -3

// The function definition looks like this:

const min = (...params) => { // params – an array containing all the provided values in the same order
 // Logic of finding the minimum number
};

This is a good example of proper usage. However, it is sometimes used incorrectly. For example, in functions that take a definite number of arguments. Below is an example of a function that compares two files:

// This function has only two parameters
const difference = checkDifference(path1, path2);

// Incorrect definition
const checkDifference = (...paths) => {
  // Logic
}

This implementation has numerous flaws. It’s not only non-obvious but also semantically incorrect. The rest syntax implies that the function supports any number of paths. However, it only supports two. This will certainly confuse those who would use the function. It is impossible to set default values with such a definition. And accessing the paths within the function becomes more difficult; in the worst-case scenario, the following code will appear: paths[0] and paths[1]. Indexes are difficult to read and make it more error-prone.

The general rule is straightforward: use the rest syntax in function definitions only when the function can handle any number of parameters of the same type.

Flags

Answer the following question: what does the second parameter in this function do?

validate(data, false);

This question is nearly impossible to answer. Parameters–flags frequently indicate poor abstraction. This happens when two or more functions are combined within a single function. The flag in this case acts as a switch between two behavior options.

In the example above, the flag means switching the predicate. It returns true or false depending on the validation success, throwing an exception in the case of errors. The correct approach is to break this function into two:

isValid(data); // predicate that returns true or false
validate(data); // function throwing an exception if something goes wrong. Inside it calls isValid

Sometimes flags are options, so it’s preferable to pass options explicitly instead:

// Bad
markdown(text, true, false);

// Good
markdown(text, { sanitize: true, autoLinks: false });

However, it may be inconvenient to work without flags sometimes. For example, consider the following function for determining an element's visibility:

setVisible(false);

The flag is appropriate here, but it’s crucial to pay attention to the function name. The current name is irrelevant because it is about making visible but it omits the other option (making hidden). This function should be called setVisibility().

Internal parameters

Additional parameters-accumulators are sometimes required in recursive functions; they are passed deeper in function in recursive calls. A common example is tree traversal. You may need to know the tree's current processed depth level when you intend to go deeper. Consider the following function, which displays the contents of a book on the screen:

const result = generateContent(data);

// The actual implementation:

const generateContent = (data, depth = 1) => {
  // Logic
  // Somewhere inside a recursive call takes place 
  return generateContent(subdata, depth + 1); // +1 because it's a new level
}

This implementation has a significant disadvantage. Although the depth parameter is not intended for use in client code, it’s visible in the definition and thus in the editor's hints and documentation. As a result, it appears to be usable but it’s not. In this case, depth is an exclusively internal parameter that of no interest for those who call this function.

This parameter remains hidden for external user when properly implemented. This is easily accomplished by creating an internal function that will be called recursively:

const generateContent = (data) => {
  const iter = (innerData, depth) => {
  // Logic
  return iter(innerSubData, depth + 1);
  };

  iter(data, 1); // Start from the first level
}

Additional materials

User avatar Kirill Mokevnin
Kirill Mokevnin 01 June 2022
1
Suggested learning programs
profession
Development of front-end components for web applications
10 months
from scratch
Start at any time
profession
Layout with the latest CSS standards
5 months
from scratch
under development
Start at any time
profession
new
Developing web applications with Django
10 months
from scratch
under development
Start at any time