Rust allows redeclaring local variables to great benefit

Monday, April 24, 2023

A lot of programming languages allow variable shadowing in new scopes. Early on, you learn that it can cause errors and can be confusing, but is situationally appropriate sometimes.

Something that's less commonly allowed is redeclaring variables to shadow them locally. And when it is allowed, it's often considered bad practice and confusing.

You're allowed to do this in JavaScript:

var x = 10;
var x;

console.log(x); // prints 10

The newer let keyword disallows this. The following code will not run:

let x = 10;
let x; // ERROR: Identifier 'x' has already been declared

Running it produces the error message "Identifier 'x' has already been declared."

This is an understandable message, because why would you redeclare something that already exists? The vast majority of the time it is a mistake and a typo, so it probably should be disallowed. This is exactly the point that Nystrom makes in Crafting Interpreters:

At the top level, Lox allows redeclaring a variable with the same name as a previous declaration because that’s useful for the REPL. But inside a local scope, that’s a pretty weird thing to do. It’s likely to be a mistake, and many languages, including our own Lox, enshrine that assumption by making this an error.

In a sidebar, he notes that Rust does allow this and idiomatic code relies on it. If it's so problematic in other languages, why does Rust allow and even encourage it?

There are a few common cases that it makes clearer. Here are a few that come to mind quickly, and there are probably many more.

  1. Making something immutable once you're done with it.
  2. Unwrapping containers while retaining clear naming.
  3. Changing types (dynamic typing vibe) while retaining clear naming.

Let's look at immutability. One thing you do somewhat often is create a list and put a few items into it. Pretending that we don't have convenient macros like vec! to build these, we would have to leave it mutable, or make a helper function for the construction. Instead, we can just... say it's not mutable anymore, basically:

let mut xs: Vec<u32> = Vec::new();
xs.push(1);
xs.push(2);
let xs = xs; // no longer can be changed!

// a few lines later

xs.push(10); // error!

Since we redeclared xs without mut, we now can detect if we try to mutate it later on. You can do the same thing in the opposite direction, too, which is handy for temporary mutability.

This pattern is really nice because it lets you be explicit about whether or not something should currently be mutable while also retaining a lot of flexibility. All the power, with a compiler that's watching your back.

Now onto the next example: Unwrapping things! Which is also changing their types! This is something you run into fairly often. You'll get back data of one type, then need to transform it to another.

Let's look at an example involving parsing an integer. You might have a (slightly simplified) function like this:

use std::str::FromStr;
pub fn get_port() -> Result<u16, std::num::ParseIntError> {
  // this is a constant here but would probably come from
  // a command-line arg or an environment variable.
  let port: &str = "8080";
  let port: u16 = u16::from_str(port)?;
  println!("Parsed port as {port}");
  Ok(port)
}

As a matter of style, you could name each of them different things. port_str just grates on my sensibilities, though. And parsed_port for the converted one is really quite unpleasant, too, in my opinion.

It's opinion, it's style, but I think it's wonderful that Rust lets us do this and keep clear (to us) names. Some people will disagree and say it's less clear. That's fine, but it's also generally idiomatic in Rust to do this, and it's also situationally dependent. Usually the redeclaration is close to the original declaration, which greatly aids in clarity.

The other thing that makes this particularly nice in Rust is the type system. In JavaScript, the type system (or lack thereof) will not save you at all if you redeclare a variable and accidentally break code that expects it to still be the old type. But with Rust, the type system will quite robustly make sure you're not messing up the types. If you redeclare an integer and now a string has that name? Great, as long as it compiles.

You get a lot of the vibe of dynamic typing, because you can change what type a particular name binds to. But you don't have as much of the danger, since things won't unexpectedly change out from under you. Flexibility with safety. That's beautiful.


If this post was enjoyable or useful for you, please share it! If you have comments, questions, or feedback, you can email my personal email. To get new posts and support my work, subscribe to the newsletter. There is also an RSS feed.

Want to become a better programmer? Join the Recurse Center!