Back to Articles

The Case Against Rebinding

Why reusing variable names undermines clarity — and how immutable bindings produce better code.

In many languages, variables can be rebound — assigned a new value after their initial definition. While convenient, this is almost always a sign of unclear thinking and leads to confusing code.

The Problem with Rebinding

Consider tracking the weight of a carrot:

weight = 150

// ... later, after taking a bite ...

weight = 142

This compiles in many languages, but it obscures what's actually happening. The weight after a bite isn't the same thing as the weight before. By reusing the name weight, we've hidden an important distinction.

What's Actually Happening

When you take a bite from a carrot, you have two distinct values:

  1. The original weight (before the bite)
  2. The remaining weight (after the bite)

These are two different values representing two different states. Giving them the same name hides this reality.

The Better Approach

Use descriptive names that reflect what each value actually represents:

original_weight = 150
bite_size = 8
remaining_weight = original_weight - bite_size

Now the code documents itself. Anyone reading it understands:

  • There was an original weight
  • Something was removed (a bite)
  • We calculated what remains

Why This Matters

1. Debugging Becomes Easier

With rebinding
weight = 150
weight = weight - 8
weight = weight - 12
// What was the original weight? We've lost it.
Without rebinding
original_weight = 150
after_first_bite = original_weight - 8
after_second_bite = after_first_bite - 12
// All values are preserved and traceable.

2. Code Reflects Reality

A 150g carrot that becomes 142g didn't have its weight "reassigned" — it lost material. Your code should reflect that distinction.

3. Concurrent Code is Safer

When values don't change, there's no risk of one part of your program seeing an old value while another sees a new one. Immutability eliminates entire categories of concurrency bugs.

A More Complex Example

Consider tracking a bank account:

Bad (with rebinding)
balance = 1000
balance = balance - 50   // withdrawal
balance = balance + 200  // deposit
balance = balance - 75   // another withdrawal
// What happened? Hard to tell.
Good (without rebinding)
initial_balance = 1000
after_withdrawal = initial_balance - 50
after_deposit = after_withdrawal + 200
final_balance = after_deposit - 75
// Clear history of transformations.

Or even better, model the transactions explicitly:

initial_balance = 1000
transactions = [-50, 200, -75]
final_balance = initial_balance + sum(transactions)

The Rule

If you find yourself rebinding a variable, stop and ask:

  1. Am I dealing with two conceptually different things?
  2. Would a new name make the code clearer?
  3. Am I trying to model state changes? (Consider a different approach)

The answer is almost always "yes" to at least one of these questions.

How Stone Handles This

Stone takes this principle to its logical conclusion: once a name is bound to a value, it cannot be rebound. There is no reassignment and no shadowing. When you genuinely need state that evolves over time, Stone provides evolve — a structured loop construct with explicit state transitions using prime notation (x'):

Stone
balance = evolve (balance = 1000) for (tx in transactions) {
    balance' = balance + tx
}

This isn't just a stylistic choice — it's also a performance enabler. Because the compiler knows values never change, it can freely reorder computations, eliminate redundant work, and parallelise operations. Immutability is one of the key reasons Stone achieves C-level performance.

Good names cost nothing but make code dramatically easier to understand, debug, and maintain. Use them generously. And when you need state changes, make them explicit — your future self (and your compiler) will thank you.