A question about scoping in impl blocks

I defined a simple struct with an impl block:

struct Location {
    line: u16,
    column: u16
}

impl Location {
    fn next_line(&mut self) {
        self.line += 1;
        self.column = 1;
    }

    fn update(&mut self, other: Location) {
        self.line = other.line;
        self.column = other.column;
    }

    fn from(line: u16, column: u16) -> Location {
        Location { line: line, column: column }
    }

    fn new() -> Location {
        Location::from(1, 1)
    }

    fn from_location(other: Location) -> Location {
        Location::from(other.line, other.column)
    }
}

My question is - why do I need to qualify the calls to from with Location::? Without that, I get compilation errors saying from can't be found, even though it's within the same impl block.

3 Likes

Technically, the reason his is required is that nobody made the compiler do otherwise. So I suppose the question is why wouldn't we want it to work that way?

Rust tends to require explicitness whenever any sort of ambiguity could arise. In this case, there's a trait std::convert::From, which is both in the std prelude and in common usage. Additionally, referring to constructors without using the constructed type's name is... an odd practice. Usually we'd only want to do that for a conversion function where the type is obvious. Besides, wouldn't it be preferable to call your function the same way everywhere it is used?

Well, ISTM that From is not ambiguous with from because Rust is case-sensitive. In this case, we're not calling a constructor from external code (where I agree, qualifying by type name would be essential) but we're inside the implementation of the type itself, so I would have thought that the type would be obvious in the context. In the same way as, you don't have to specify a type for self, because it's obvious in the context.

Why should one not need to specify the type explicitly in this case? Rust is verbose enough as it is - why add to the noise? If there really were an ambiguity, I would expect Rust's generally informative error messages to say where the ambiguity is, whereas the error message I get is

   |
27 |         from(1, 1)
   |         ^^^^ not found in this scope

which is confusing, because the actual code is just adjacent:

    fn from(line: u16, column: u16) -> Location { // <-- this is the from I'm referring
        Location { line: line, column: column }   //     to - seems to be in scope!
    }

    fn new() -> Location {
        from(1, 1)       // <-- error occurs on this line - line 27
    }

It is not in Scope, the scope of names is the module, not the impl block.

Sadly you can't even use the impl block as you were able to with enum variants...

That does seem sad. TIL. I thought it was a block-scoped language, but clearly, not as I understand the term. I wonder what the reasoning for this restriction is, surely it can't be "we didn't think to allow it".

The method From::from is ambiguous with the method Location::from. If you see from unadorned, one expects it to be the former, not the latter, because it is imported in the prelude. (In fact, I would tend to expect you to use the former in this code rather than defining an inherent method to do the same job.)

In this case, we’re not calling a constructor from external code (where I agree, qualifying by type name would be essential) but we’re inside the implementation of the type itself, so I would have thought that the type would be obvious in the context.

I would agree with you... until your context gets spread over a 10K line file and someone else is looking at it 5 years later. They won't want to see from and be forced to analyze your imports trying to find out where it comes from. In general, this isn't really a problem because using the name of the type to construct it is idiomatic and clarifying.

1 Like

You can often use Self to avoid typing longer names.

As with &mut self, and self.foo, both which are unnecessary in some languages, Rust has just chosen to be explicit. When you see Location::from you know it's that method, and any other from function that is global or has been imported from another module.

In simple cases this is an overkill, but in bigger programs people do get confused about such things, and e.g. in C++ people tend to use m_from for methods.

2 Likes

Ah, but this isn't production code or anything, just stuff I'm trying to do to learn Rust (this is literally my first attempt at using Rust). My understanding of block scoping (in general - maybe Rust differs) is that names redefined in inner blocks shadow and override the same names in outer blocks, and it seems that Rust is happy for this to happen even in the same block when you're inside a block inside a fn ...

No argument there if I'm calling external code to construct something. The analogue of what I'm doing here in other languages is to use one constructor from another of the same type, which is why it was surprising that you have to be explicit here - but, now I know. Thanks for the explanation.

1 Like

That's a handy tip, thanks.

IIUC this is a holdover from the Hungarian notation popularised by Microsoft (and now widely regarded as unnecessary) , I'm not sure how much traction the practice has outside that sphere of influence.

1 Like

My understanding of block scoping (in general - maybe Rust differs) is that names redefined in inner blocks shadow and override the same names in outer blocks, and it seems that Rust is happy for this to happen even in the same block when you’re inside a block inside a fn

I don't know that there's a general rule for that. Within a function, variables can shadow neatly because you can model each sequential line of code as the start of a new (implicit) block, all terminating at the end of the function. The important thing here is that the code within a function is ordered. Later declarations shadow earlier ones.

Within an impl block or module, the definitions are not ordered, so it's not obvious what should shadow what. Even the idea that modules and impl blocks are arranged hierarchically can be subverted by use statements, which are not allowed at the impl block level. Effectively, what is in scope within a function is determined by the parent module, not the impl block. The impl block is fairly transparent... it doesn't do much more than establish an association between types and functions.

For instance, you could have written this and it would work exactly the same as your existing code:

impl Location {
    fn from(line: u16, column: u16) -> Location {
        Location { line: line, column: column }
    }
}

impl Location {
    fn new() -> Location {
        Location::from(1, 1)
    }
}

But now they're in different blocks. In fact, you could even put them in different modules if you wanted.

2 Likes