Question
I saw this Rust code:
fields.sort_by_key(|&(_, ref field)| field.tags().into_iter().min().unwrap());
let fields = fields;
What does the line let fields = fields; do, and why would someone write it after sorting the vector?
Short Answer
By the end of this page, you will understand that let fields = fields; in Rust creates a new binding with the same name, a feature called shadowing. You will learn how shadowing differs from assignment, why it is often used to change mutability, ownership, or type context, and why this pattern appears in real Rust code.
Concept
In Rust, this line:
let fields = fields;
does not mean “assign the variable to itself” in the usual sense.
Instead, it creates a new variable binding named fields and initializes it using the old fields.
This is called shadowing.
What shadowing means
When you write:
let x = 5;
let x = x;
the second x is a brand new binding. It reuses the same name, but it is not the same variable as the first one.
Rust allows this because let always creates a new binding.
Why this matters
Shadowing is useful in Rust because it lets you:
- keep the same variable name while transforming a value
- make a mutable variable effectively immutable afterward
- change the type associated with a name
- move ownership into a new binding
- limit how long an older binding remains visible
In the original example
fields.sort_by_key(|&(_, ref field)| field.tags().into_iter().min().unwrap());
let fields = fields;
The call to sort_by_key requires fields to be mutable, because sorting changes the collection in place.
A common pattern is:
let mut fields = ...;
fields.sort_by_key(...);
let fields = fields;
After sorting, the developer may no longer want fields to be mutable. Writing let fields = fields; creates a new immutable binding with the same value.
So the line is often used to say:
- “I needed this variable to be mutable for a moment”
- “Now I want to keep using it as immutable”
That improves clarity and can prevent accidental mutation later.
Mental Model
Think of a variable name in Rust like a label attached to a box.
- First, you have a box labeled
fields. - Then Rust lets you create a new box and attach the same label
fieldsto it. - From that point on, when you say
fields, you mean the new box, not the old one.
For let fields = fields;, imagine this:
- You have a temporary workbench copy labeled
fieldsthat you are allowed to rearrange. - Once you finish rearranging it, you put the result into a clean final slot, also labeled
fields. - From then on, you use the final version.
So the code is often less about changing the value and more about changing how the program is allowed to use that value.
Syntax and Examples
The general syntax is:
let name = value;
let name = name;
This is shadowing.
Basic example
fn main() {
let x = 10;
let x = x;
println!("{}", x);
}
This prints:
10
The value stays the same, but the second let x = x; creates a new binding.
Common real use: remove mutability
fn main() {
let mut numbers = vec![3, 1, 2];
numbers.sort();
let numbers = numbers;
println!("{:?}", numbers);
}
What this means
numbersis mutable at first so it can be sorted.- After sorting,
let numbers = numbers;creates a new immutable binding. - From then on, you cannot accidentally modify
numbers.
Another shadowing example with transformation
fn main() {
let input = "42";
let input = input.parse::<i32>().unwrap();
println!("{}", input + 1);
}
Here shadowing is used to keep the same name while changing the value from &str to i32.
Important: this is not normal assignment
Rust assignment looks like this:
let mut x = 1;
x = 2;
Shadowing looks like this:
let x = 1;
let x = 2;
Assignment changes an existing mutable variable. Shadowing creates a new variable binding.
Step by Step Execution
Consider this example:
fn main() {
let mut fields = vec![3, 1, 2];
fields.sort();
let fields = fields;
println!("{:?}", fields);
}
Step-by-step
1. Create a mutable vector
let mut fields = vec![3, 1, 2];
- A variable named
fieldsis created. - It is mutable because of
mut. - Current value:
[3, 1, 2]
2. Sort the vector in place
fields.sort();
- The vector is rearranged.
- Current value becomes
[1, 2, 3] - This works because the binding is mutable.
3. Shadow it with a new immutable binding
let fields = fields;
- A new binding named
fieldsis created. - It takes the value from the old
fields. - The old binding is now shadowed.
- The new binding is immutable because it does not use
mut.
4. Use the new binding
println!("{:?}", fields);
- This refers to the new immutable
fields. - Output:
[1, 2, 3]
What if you try to mutate afterward?
fn main() {
let mut fields = vec![3, 1, 2];
fields.sort();
let fields = fields;
fields.push(4);
}
This will fail because the new fields is immutable.
Real World Use Cases
Shadowing with let x = x; appears in real Rust programs for a few practical reasons.
1. Temporary mutability
A value may need to be mutable only during setup.
let mut config = load_config();
config.normalize();
let config = config;
After normalization, the developer wants to prevent further changes.
2. Ownership handoff into a final binding
Sometimes a value is built or modified in stages, then rebound as the final version.
let mut items = fetch_items();
items.sort();
let items = items;
This makes it clear that the editable phase is over.
3. Keeping the same variable name after a conversion
let data = read_input();
let data = data.trim();
let data = data.parse::<u32>().unwrap();
This avoids inventing names like data2 or parsed_data when the older versions are no longer needed.
4. Narrowing scope of an older binding
Shadowing lets developers move from one stage of processing to another while keeping code readable.
For example:
- raw input
- cleaned input
- validated input
- parsed input
Each stage can reuse the same name if the earlier form is no longer useful.
Real Codebase Usage
In real Rust codebases, shadowing is a normal and idiomatic tool.
Common pattern: mutable setup, immutable use
One of the most common uses is this pattern:
let mut data = build_data();
data.sort();
data.dedup();
let data = data;
This communicates intent clearly:
- mutation is allowed during preparation
- mutation is not expected afterward
Builder-like workflows
Developers often process values in multiple steps:
let request = request.headers(headers);
let request = request.body(body);
This keeps a single meaningful name while each step produces a new value.
Validation and parsing pipelines
let input = input.trim();
let input = if input.is_empty() { return Err("empty input"); } else { input };
let input = input.parse::<u64>()?;
This is common in CLI tools, web handlers, and configuration loading.
Guarding against accidental mutation
Even when Rust already enforces safety, developers still use immutability to make intent explicit.
A shadowed immutable binding can make maintenance easier because later readers know the value should not change.
Why not always just keep it mutable?
Because mutable state is harder to reason about. Limiting mutability to the smallest possible region makes code easier to understand and less error-prone.
Common Mistakes
1. Thinking it is a no-op
Beginners often assume this line does nothing:
let fields = fields;
But it creates a new binding. That can affect:
- mutability
- type
- ownership
- visibility of the old binding
2. Confusing shadowing with assignment
Broken understanding:
let x = 5;
let x = 6;
This is not changing the first x. It creates a second x.
Compare with assignment:
let mut x = 5;
x = 6;
Use assignment when you want to update an existing mutable variable. Use shadowing when you want a new binding.
3. Expecting mutability to carry over automatically
fn main() {
let mut x = 1;
let x = x;
x = 2;
}
This is broken because the new x is immutable.
To keep mutability, you must write:
let mut x = 1;
let mut x = x;
x = 2;
4. Overusing shadowing so code becomes hard to follow
This is legal but can get confusing:
let data = read();
let data = clean(data);
let data = transform(data);
let data = validate(data);
This can be fine in short pipelines, but in long sections it may hide what type or state data currently has.
5. Forgetting that moves may happen
For owned values, the old binding may be moved into the new one:
let s = String::from("hello");
let s = s;
This is fine because the old s is immediately shadowed, but it still matters conceptually: ownership has moved into the new binding.
Comparisons
| Concept | Syntax | Creates new binding? | Requires mut? | Can change type? |
|---|---|---|---|---|
| Shadowing | let x = x; or let x = expr; | Yes | No | Yes |
| Assignment | x = expr; | No | Yes | No |
Shadowing vs assignment
let mut x = 10;
x = 20;
- updates the same variable
- needs
mut - keeps the same type
let x = 10;
let x = 20;
- creates a new binding
- does not require the old one to be mutable
- may reuse the same name
Shadowing vs using a different name
let mut fields = get_fields();
fields.sort();
let sorted_fields = fields;
This is also valid.
Trade-off
let fields = fields;keeps the name short and consistent.let sorted_fields = fields;may be clearer if the new meaning is important.
Shadowing vs inner scopes
let x = 1;
{
let x = 2;
println!("{}", x);
}
println!("{}", x);
Inner scopes also allow shadowing, but the outer binding becomes visible again after the block ends. With sequential shadowing, the older binding stays hidden for the rest of the scope.
Cheat Sheet
Quick reference
Shadowing
let x = 5;
let x = x;
- creates a new binding
- reuses the same name
- old binding is shadowed
Common use: remove mutability
let mut items = vec![3, 1, 2];
items.sort();
let items = items;
After this, items is immutable.
Assignment is different
let mut x = 1;
x = 2;
- modifies existing variable
- requires
mut - cannot change type
Shadowing can change type
let x = "42";
let x = x.parse::<i32>().unwrap();
Rules to remember
letalways creates a new binding.mutdoes not carry over unless you write it again.- Shadowing is often used to limit mutability.
- Reusing the same name is idiomatic in Rust when earlier versions are no longer needed.
In the original example
fields.sort_by_key(...);
let fields = fields;
Most likely meaning:
fieldsneeded to be mutable for sorting- after sorting, the author wanted an immutable
fields
FAQ
Why would someone write let fields = fields; in Rust?
Usually to create a new binding with the same value, often changing it from mutable to immutable after a preparation step.
Does let x = x; copy the value?
Not always. It depends on the type.
Copytypes are copied.- Non-
Copytypes are moved.
Either way, the result is a new binding.
Is let x = x; considered idiomatic Rust?
Yes, in the right context. It is especially common when shadowing after parsing, transforming, or temporarily mutating a value.
Is this the same as x = x;?
No.
let x = x;creates a new binding.x = x;would be assignment to an existing mutable variable.
Can shadowing change a variable from mutable to immutable?
Yes.
let mut x = vec![1, 2, 3];
x.sort();
let x = x;
Now x is immutable.
Can shadowing change the type of a variable?
Yes.
let x = "42";
let x = x.parse::<i32>().unwrap();
The first x is &str; the second is i32.
Why not just keep using a mutable variable?
Because limiting mutability makes code easier to reason about and helps prevent accidental changes later.
Mini Project
Description
Build a small Rust program that creates a list of numbers, sorts it, and then uses shadowing to make the sorted list immutable. This demonstrates the exact idea behind let fields = fields; in a practical, easy-to-run example.
Goal
Create, mutate, and then rebind a vector so that it can no longer be modified after setup.
Requirements
Create a mutable vector with unsorted numbers.
Sort the vector in place.
Shadow the vector with let numbers = numbers;.
Print the final sorted vector.
Show that further mutation would not be allowed.
Keep learning
Related questions
Absolute Values in Rust: Using the abs() Method
Learn how to calculate absolute values in modern Rust with abs(), handle signed integer overflow, and choose safer alternatives.
Accepting Iterators in Rust Functions with impl Iterator
Learn how to write Rust functions that accept HashMap iterators using generic bounds or impl Iterator, avoiding trait object and lifetime errors.
Access Enum Variant Values in Rust with Pattern Matching
Learn how to extract and print values stored in Rust enum variants using match and if let pattern matching, with practical examples.