Stop Shadowing and Rebinding Variables

Michael Stalker
Code for RentPath
Published in
4 min readMar 12, 2021

--

Photo by Bernard Tuck on Unsplash

Variable Shadowing

variable shadowing occurs when a variable declared within a certain scope…has the same name as a variable declared in an outer scope.

– From “Variable shadowing” on Wikipedia

Consider this Elixir code:

defmodule Shadowing do
x = 5
def x, do: x
def x(x = 0), do: x
def x(x), do: x(x - 1)
end

Without running the code, tell me what the return values of these three function calls are:

  1. Shadow.x()
  2. Shadow.x(0)
  3. Shadow.x(2)

No, really. Think about the code for a minute.

Now…are you positive your answers are right?

Sometimes, x refers to a function. Sometimes it refers to a function parameter. Once, it is a temporary variable that gets evaluated in a module context at compile time. The meaning of x changes, depending on the context.

Rebinding Variables

In Elixir, rebinding happens when you assign a new value to a variable that you’re already using. For example:

def calculate_name do
name = "Chloe"
# Perform some calculations... name = "Charlotte" # Perform some calculations... name
end

What’s Wrong With That?

Shadowing variables and rebinding variables are suboptimal for a few reasons:

  1. It requires you to know about the language’s scoping rules.
  2. It associates different concepts with the same name.

Scoping Rules

One could say that people should know the scoping rules of the languages they use. That’s absolutely right. However, language knowledge takes time to acquire. It can be confusing to read code like the first example above before acquiring a certain level of language proficiency. Or, worse, a new developer may wrongly assume he or she knows how it works.

Should developers therefore limit themselves to well-known language features? Not necessarily. However, it is often easy to recognize unfamiliar language constructs. For instance, when I saw Array#abbrev in Ruby, I immediately identified it as a method I didn’t know. At that point, it’s a simple matter to read the documentation to learn what it does.

Scoping rules are not like that. They are hidden. When you read some code that relies on these rules, you might not even realize that your assumptions about the code are wrong. None of the code you’re focusing on may say to you, “You need to look into this. There’s more going on than you realize, and you’re going to introduce a bug into the system.”

Even if you are an advanced coder, you still might make a careless mistake that stems from shadowing variables. Maybe you’re coding while you’re tired. Maybe you just switched from one language to another, and the scoping rules are different. Regardless, the result is the same. You can unwittingly introduce a defect into the system.

Identifier Confusion

Have you ever heard someone say your name, only to realize the speaker was addressing someone else? The fact that someone else had the same name as you led to a confusing situation. It requires you to think harder when two distinct entities share the same identifier.

Code is no different. Names help us think about ideas. They provide an easy way to identify a concept and to talk about it. When we reuse a name, we make the code harder to reason about.

When It’s Okay To Rebind Variables

It’s fine to rebind a value to a variable when the variable’s sole purpose is to accumulate a value.¹ For example:

items = ["shoe"]
items = ["shirt" | items]
items = ["hat" | items]

We create a list with one element, and then begin adding elements to the head of the list. The example is contrived, and we could have more easily created the list with all three elements in one step. However, there are situations where you’ll build up a collection over time, and it can make sense to assign values to the same variable multiple times.

You can do this with other types of accumulators, as well. Consider some code that counts the French-suited playing cards in a deck, by suit:

card_count = length(hearts)
card_count = card_count + length(spades)
card_count = card_count + length(diamonds)
card_count = card_count + length(clubs)

The value of card_count changes, but the variable always refers to the same thing.

If you’re familiar with Elixir, a Plug.Conn struct often acts like an accumulator for connection information.

I’ll note that in a functional paradigm, you may find more idiomatic ways to accumulate values than the two examples above.

Suggestions

  1. Don’t give function parameters or variables the same name as other functions in your module.²
  2. Use () when you call a function. This makes it clear that you’re not working with a local variable.
  3. Don’t rebind values to a variable unless you’re using an accumulator.
  4. If you use an accumulator, don’t spread the rebinding code all across a function. This makes it harder to spot where the value changes.

¹ Refactoring: Ruby Edition, referring to Kent Beck’s Smalltalk Best Practices, calls these “collecting temporary variables.”

  • Jay Fields, Shane Harvie, and Martin Fowler, with Kent Beck. Refactoring: Ruby Edition. Upper Saddle River, N.J.: Addison-Wesley, 2010. pg.122.
    Kent Beck.
  • Smalltalk Best Practices. Upper Saddle River, N.J.: Prentice Hall, 1997.

² Defining multiple clauses for a function is fine. You still have to figure out which clause will handle a function call, but it’s always clear that you’re calling a function. There’s no confusion caused by scoping rules.

--

--