Improve Code Readability by Using Parameter Modes

Improve Code Readability by Using Parameter Modes Featured Image

Parameter modes are a way of improving code readability by indicating how a parameter might change as the result of a function call. They’re useful for indicating side effects, and I promise your users will love them. Let’s talk about it!

Table of Contents

Introducing Code Readability

In the world of software development, we often stress more than just code correctness. After all, depending on its use, code tends to outlive the afternoon we took to write it. As a result, it’s equally as important to consider how to make code as readable as possible for others as well as our future selves.

Unfortunately, readability is one of those concepts that’s highly contested. What does it mean for code to be readable? How do we write code that is readable?

I don’t have the answers to those questions, but I can say that there are a lot of rules around readability that may or not work. For instance, a lot of folks say it’s important to comment code. Likewise, folks generally agree that naming conventions are important.

Today, I want to introduce yet another tool to your readability toolkit: parameter modes. However, for me to do that, I need to introduce a few concepts first.

Laying Some Groundwork: Imperative vs. Functional Programming

At one point in my life, I wrote extensively about the differences between expressions and statements. In short, expressions are code segments that can be evaluated to a value (e.g., 3 + 7). Meanwhile, statements are code segments that change the state of our program (e.g., int x = 10;).

Statements are an idea that’s unique to imperative programming. Under imperative programming, we write programs the same way we might write a recipe (i.e., in a series of steps). Each step in a recipe changes the state of the food. For example, mixing flour and water is a change in the state of those two ingredients. As a result, there is no way we can repeat that step because the materials are already mixed. Of course, we need the mixed ingredients to move on to the next step.

Ultimately, imperative programming is kind of like how a hairdresser can always go shorter but can never lengthen. Cutting hair is a state change that depends on its previous state (also, apologies for mixing cake and hair analogies).

In contrast, functional programming removes the idea of statements altogether: everything is an expression. Solutions can then be written as one large expression. This is typically not how we write a recipe because recipes have implied stateOpens in a new tab.. That said, here’s what one user, Brent, shared for a cake baking function:

cake = cooled(
  removed_from_oven(
    added_to_oven(
      30min, 
      poured(greased(floured(pan)), 
      stirred(
        chopped(walnuts), 
        alternating_mixed(
          buttermilk, 
          whisked(flour, baking soda, salt), 
          mixed(
            bananas, 
            beat_mixed(eggs, creamed_until(fluffy, butter, white sugar, brown sugar))
          )
        )
      )
    )
  )
)

As you can see, a functional recipe actually works backwards. We want a cake, so we work backward from having it. The last step is cooling a complete cake which comes from the oven which was added to the oven… you get the idea! This is how an expression works; we compute the innermost sections first. In other words, we find the smallest problem we can solve, and we solve that first.

Before we move on, I should mention that not all functional languages operate this way. The nesting of expressions is something that comes from Lisp, but there are plenty of modern functional languages that have structures similar to what we might see in a recipe. They’re called pipes (e.g., `|`), and they’re used to “pipe” output of one function to the next.

Of course, the goal here isn’t to explain the difference between imperative and functional programming. It’s to show that there are things we can learn from the distinction between imperative and functional programming that allow us to write better code. For example, in functional programming, we can be certain that functions will behave predictably (i.e., if we know the inputs, we can predict the output).

Drawing Meaningful Distinctions: Functions vs. Procedures

The idea of a predictable function, often called a pure function, isn’t unique to functional programming. You can make pure functions in an imperative programming language as well:

def square(num: float) -> float:
  return num * num

This square function in Python is a pure function; it accepts an argument and returns a value. In other words, it operates exactly like an expression. Contrast that with what we call a procedure:

def reset(nums: list) -> None:
  nums.clear()

In this example, we have a procedure that takes a list of numbers and makes the list empty. Nothing is returned, but state is changed. Therefore, a procedure is a statement.

Of course, in an imperative programming language like Python or Java, there is no syntactic difference between a procedure and a function. As a result, it’s possible to create an impure function (i.e., a function that changes state):

def sum_and_clear(nums: list) -> float:
  total = sum(nums)
  nums.clear()
  retutn total

In this example, we take a list, sum up all the elements, clear the list, and return the total. In other words, not only do we return a value, but we also clear the parameter. The clearing of the list is what’s known as a side effect, which one of my students defined as “an unintended consequence.” What can happen is that someone might use this “function” thinking it will return them a sum and not realize that it will also delete all of their data. That’s an unintended consequence of using this “function.”

Warning Users of Side Effects With Parameter Modes

Because most popular programming languages are imperative in nature, side effects are a necessary evil. After all, procedures serve an important purpose. That said, not every chunk of code we write is going to fit neatly into our function and procedure bins, so what do we do?

In a course I teach, we follow design by contract. Under design by contract, we write functions and procedures with our users in mind. In other words, we argue that as long as our user follows the necessary preconditions, we will give them the expected postcondition. We indicate this through documentation (i.e., @requires and @ensures).

With that said, even properly documenting preconditions and postconditions is not enough to warn the user of side effects. Sure, they might be implied, but to be explicit, we should tell our users which parameters are going to change. To do that, we use parameter modes.

A parameter mode is basically an indicator of whether or not a parameter will change and how. There are four of them, and they look like this:

  • Restores: parameter holds the same value before and after the function call
  • Clears: parameter value is changed to some default value (e.g., 0)
  • Updates: parameter value is changed based on its initial value (e.g., incremented)
  • Replaces: parameter value is changed regardless of its initial value (e.g., copied to)

Restores is the default parameter mode. Therefore, a function is considered pure if all parameters are in restores mode. Any other parameter mode indicates that the function is either impure or is a procedure.

Parameter Modes in Practice

One of my favorite examples of parameter modes comes from the `divide()` method of NaturalNumber, an OSU-specific component that represents the counting numbers (note: line 7 is where we actually tell the user our parameter modes):

/**
 * Divides {@code this} by {@code n}, returning the remainder.
 *
 * @param n
 *           {@code NaturalNumber} to divide by
 * @return remainder after division
 * @updates this
 * @requires n > 0
 * @ensures <pre>
 * #this = this * n + divide  and
 * 0 <= divide < n
 * </pre>
 */
NaturalNumber divide(NaturalNumber n);

This is one of the first methods that students are exposed to as they learn about mutable data types. Incidentally, it’s also one of the first methods they are exposed to that is both a function and a procedure.

If you look carefully at the contract, you’ll see that the `divide()` method changes the input value and returns a value. In this case, it computes division in the NaturalNumber that calls it and returns a remainder.

As you can imagine, once students find out that this method returns the remainder, they use it as an expression. Given what we know now, using `divide()` as an expression is deeply problematic because it has an unintended consequence (i.e., a side effect) of also changing the value of the number that called it.

Funnily enough, there really isn’t much of an issue going the other way. Using `divide()` as a procedure is generally not a big deal unless you need the return value for something. Otherwise, it can be thrown away. Problems only arise when the method is used as a function (i.e., an expression).

To ensure that students are away of this side effect, we include the `@updates` parameter mode in the method contract. That way, they can be sure that `this` will change. To see exactly how it will change, the user has to read into the postcondition.

Bringing It All Home

As programming languages have grown and developed, features have been borrowed and shared. As a result, we end up with programming languages that have some very handy features with some equally nasty quirks.

To address these quirks, we have to do our due diligence to ensure folks who read our code and documentation can make sense of it. There are many ways to do this, but today I’m advocating for parameter modes. That way, folks know whether or not a function has a side effect at a glance.

There are many ways to include parameter modes in your code, but I might recommend putting them alongside your parameter documentation. Here’s what that might look like in Python:

def accumulate(values: list) -> float:
  """
  Given a list of numbers, computes the total and adds it
  to the end of the list. 

  :param list values: (updates) a list of numbers
  :return: the sum of the original list
  """
  total = sum(values)
  values.append(total)
  return total

Alternatively, you can create a separate item just for the parameter modes (see also the Java documentation above):

def accumulate(values: list) -> float:
  """
  Given a list of numbers, computes the total and adds it
  to the end of the list. 

  :updates: values
  :param list values: a list of numbers
  :return: the sum of the original list
  """
  total = sum(values)
  values.append(total)
  return total

With that said, that’s all I have for you today. If you found this article helpful, even if a bit rambly, I would appreciate it if you gave it a share. And if you’d like to go the extra mile, check out my list of ways to grow the site. There, you’ll find links to my Patreon and YouTube channel.

As always, here are some related articles for your perusal:

Otherwise, thanks for hanging out. See you next time!

Jeremy Grifski

Jeremy grew up in a small town where he enjoyed playing soccer and video games, practicing taekwondo, and trading Pokémon cards. Once out of the nest, he pursued a Bachelors in Computer Engineering with a minor in Game Design. After college, he spent about two years writing software for a major engineering company. Then, he earned a master's in Computer Science and Engineering. Today, he pursues a PhD in Engineering Education in order to ultimately land a teaching gig. In his spare time, Jeremy enjoys spending time with his wife, playing Overwatch and Phantasy Star Online 2, practicing trombone, watching Penguins hockey, and traveling the world.

Recent Posts