Implicit declaration of vars

started reading the Rust book and just on the first few chapters, i get the impression that everything in Rust must be explicitly declared/initialized. then here comes this snippet:

fn main() {
    let a = [5; 10];
    let mut sum = 0;

    for z in a {
        println!("z now is {z}");
        sum += z;
    }

    println!("{sum}");
}

where does the var "z" come from? as i understood it, unlike in C++, for vars in Rust gets declared (and initialized) from the range? if it does, seems it'll be hard to maintain such implicit declarations.

why not keep var declarations uniform and do away with the "let" statements or the parameter var declarations? make them all implicit.

It's pretty explicitly defined by, and thus limited in scope to, the for loop.

For at least these reasons:

  • Rust favors explicitness over implicitness as a matter of core language philosophy
  • Rust is stable, and has been so since 1.0. This means that code written for 1.0 should still compile now. The changes you suggest would break such backwards compatibility, and thus is dead on arrival based on that alone
2 Likes

There's no range here so I don't know what you mean by that, but see here for a notional example of how z is declared. z can actually be a pattern which binds one, many, or no names. As the reference says, other places that this happens are with let (as you know), function parameters, and in matches.

So it's not implicit, it's explicit in any given pattern.

4 Likes

for(;;) loop is such an old syntax. Every languages actively developed nowadays have iterator based for loop, including C++.

1 Like

i was gunning for this:

for (int i; i<10; i++)....

to me, that's explicit. it's written down.

1 Like

But for pat in iter is explicit too: a pattern always introduces new bindings. It never mutates existing variables. The for is a red herring; what matters is that it's a pattern.

Or would you argue that we should need let in all pattern matches? Even in match or better yet, if let?

if let Some(let x) = foo() {
    ...
}

Seems excessive.

6 Likes

What's explicit here is that:

  • you're declaring a variable
  • you're iterating as long as that variable is less than 10
  • you're incrementing that variable at the end of each iteration

The fact that you're iterating over a range from 0 to 10 is however implicit.

Meanwhile in Rust you're explicitly iterating over the range 0..10. The variable z doesn't come out of thin air, but is introduced by the z in the for z in. It's literally the only thing that z does, so how can that not be explicit?

4 Likes

"z" just came out of nowhere. unlike the vars "a" and "sum", they were declared.

Depends on what you consider a declaration of a variable. For me for z in ... is a declaration of the variable z. What would it be otherwise? It's doing nothing else.

7 Likes

I think once you understand the "for" loop in rust a bit it becomes obvious. Coming from "c' myself, (k&r) it was a bit strange to have the "for" create a variable.
But then I realized rust is not "c". It just looks kind of like "c".

In the book it does explain this syntax. The "for" loop in rust does not do what the "c" "for" loop does. It does not have three statements inside of parenthesis. The rust "for" loop will iterate through a collection of objects, in this case the array "a" . It creates an object of the type in the collection. The rust "for" loop is the implicit declaration of the variable "z"

2 Likes

One interesting consequence of the fact that for z in … indeed (always![1]) introduces a new variable z, and that Rust allows shadowing, is that code like

fn main() {
    let a = [5; 10];
    let mut sum = 0;

    let z = 42;

    for z in a {
        println!("z now is {z}");
        sum += z;
    }

    println!("{sum}");

    println!("{z}");
}

compiles fine, and the last line does print 42, since the z (defined to be 42) outside of the loop has nothing to do with the z that is introduced by the for z in … syntax itself, and that is available (and used) inside of the loop.


  1. and thus it’s a very explicit thing; it never does something else than introducing a new variable, it doesn’t do anything like implicitly filling in a declaration, on-demand, only if it’s missing ↩︎

7 Likes

One important feature of Rust is type inference (Wikipedia).

Regarding declaring the variable, that's done by the for statement. But what I find curious is that it's not possible to explicitly specify a type:

fn main() {
    for x: usize in 0..10 {
        println!("{x}");
    }
}

(Playground)

Errors:

[…]
error: expected one of `@` or `|`, found `:`
 --> src/main.rs:2:10
  |
2 |     for x: usize in 0..10 {
  |          ^ ----- specifying the type of a pattern isn't supported
  |          |
  |          expected one of `@` or `|`
  |
[…]

For more information about this error, try `rustc --explain E0425`.
[…]
2 Likes

It's not all that curious. The type of z entirely depends on the type of the iterator, and the only case where changing it's type annotation would change the type of the iterator is if you have a range instantiated witn integer literals.

Okay, I always wondered why Item is (or needs or should be) an associated type of Iterator, and not a type parameter of the Iterator trait. I guess it would create a lot of type inference issues if it was.

1 Like

That's not true. For example:

trait Foo: Sized {
    fn foo() -> [Self; 2];
}

impl Foo for &'static str {
    fn foo() -> [Self; 2] {
        ["abc", "def"]
    }
}

impl Foo for i32 {
    fn foo() -> [Self; 2] {
        [14, 15]
    }
}

fn main() {
    // for z: &str in Foo::foo()
    for z in Foo::foo() {
        let z: &str = z;  
        println!("{z}");
    }
}
3 Likes

you're taking it too far. i'm referring to the for loop only

for (int i; i<10; i++)....

if your if let expression there's no need as you already declared a var using the let keyword.

Here's in an interpretation that you might find more helpful.

let is not the only way to create a binding in Rust. Each declaring keyword has different effects.

// `const` means a value is known to exist at compile time 
// (but could be compiled out)
const FOO: usize = 0;

// `static` means a value is somewhere in static memory
static BAR: usize = 1;

// `let` declares a binding in the local scope
let baz = 2;

// `match` binds variables only if certain patterns are met
match baz {
    0 => println!("blah"),
    quux => println("i have a quux: {quux}"),
};

// `for` binds a variable to an iterator item
for zorp in [FOO, BAR, baz] {
    println!("{zorp}");
}

If we had for let, consistency would (in my opinion) require const let and static let and so on. This would probably erase the meaning of let. This more or less comes down to taste, so Rust favors the consistency of each keyword having its own properties.

But there's no need for it in a for loop, either. for also unconditionally introduces a new binding, as others and I have repeatedly explained it. So it's not necessary to add one more keyword.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.