Allow shadowing self in methods

I quite often find myself working with methods that have receivers as follows:

fn example(self: &Arc<Self>) {
    // ...
}

I would like to be able to shadow self and use it as follows:

fn example(self: &Arc<Self>, debug: bool) {
    if debug {
        let self = self.clone(); // get an owned Arc<Self>
        run_something(self);
    }
}

This example is a bit contrived, but when designing applications using FRP, e.g., from the futures-signals library, I find this comes up a lot and forces writing everything as associated functions instead of methods.

fn example(this: &Arc<Self>, debug: bool) {
    if debug {
        let this = this.clone(); // get an owned Arc<Self>
        run_something(this);
    }
}

and at the call site:

Struct::example(&instance, true) // instead of instance.example(true)

Is there a fundamental reason why self cannot be shadowed?

4 Likes

I would like to flip it around: why is it insufficient to just name the binding something else, rather than shadowing?

Here's one: shadowing self would mean the actual method receiver, while technically still in scope, is no longer reachable. That in turn could prevent calling other methods, depending on the type of the value that is bound to the binding doing the shadowing.

I don't understand how not being able to shadow self affects the use of methods vs associated functions. Why can't you combine your second and third code blocks to this?

fn example(self: &Arc<Self>, debug: bool) {
    if debug {
        let this = self.clone(); // get an owned Arc<Self>
        run_something(this);
    }
}
2 Likes

@sfackler @jjpe I find the issue comes up when using a macro such as pin that tries to shadow the variable by rebinding it under the same name.

2 Likes

I am not saying that this issue is impossible to work around, just that from a developer's perspective it feels like an unnecessary and somewhat arbitrary restriction in the language.

4 Likes

Here's one: shadowing self would mean the actual method receiver, while technically still in scope, is no longer reachable. That in turn could prevent calling other methods, depending on the type of the value that is bound to the binding doing the shadowing.

Sometimes that is the intended effect of shadowing. Pinning, for example, exploits this.

3 Likes

This could be said about any variable, dismissing the concept of shadowing itself.

6 Likes

But that's not what the pin macro does. pin!(self) expands to a Pin<&mut Self> but leaves self itself alone. Typically you'd assign the result to a new binding, for example:

let pinned = pin!(self);

Probably talking about pin in tokio - Rust instead of the unstable pin in std::pin - Rust from std.

1 Like

Ah yes, silly me.

You can use macros that rebind identifiers by simply renaming it beforehand

let this = self;
pin_mut!(this);

But, tbh I don't understand why the self binding is special in this way. There's many situations where I have been forced to use this inside a method to workaround not being able to shadow it.

1 Like

I would find it more surprising than normal to rebind self to a different type as now when I see self.foo() I can't be sure I know what it's doing without reading all preceding code. Does self have any relation to Self anymore?

Maybe it wouldn't be so bad if limited to valid receiver types.

1 Like

Macro corner cases strike me as a good technical reason for this change. The papercut of having to rename things to this in normal code does not seem like a super compelling reason; macros breaking because you're using the wrong variable name certainly does.

As far as being able to rename self becoming confusing, perhaps, but the usual rejoinder of "you can write confusing code already" seems to fit. I often find shadowing in rust to be clarity inducing, I expect it will be the same for self (albeit minimally).

5 Likes

I think the principle of least surprise is also worth considering here, I certainly found it surprising the first time I bumped into this. It just seems inconsistent -- any other argument to a method can be rebound, why not self?

1 Like

Would you only allow this as shadowing in methods, or also let self and other patterns in general?

Also note that it's currently so special that you can't even use raw r#self!

2 Likes

self is special though. Its type signature is weird and it gets highlighted. It's not especially surprising that there are other special things that go along with that.

It does get funky doesn't it. But probably it would be allowed anywhere. This might also enable things like

// Not in an impl block!
fn fun(self: &A, b: &B) {
    ...
}

That is indeed strange, but perhaps such uses could be discouraged with a warning as opposed to not allowing them.

(NOT A CONTRIBUTION)

I'd be interested in knowing the history here. Why was shadowing self forbidden? Was it an oversight, or was it an intentional decision based on some concern? I remember shadowing was a controversial feature originally (Graydon didn't like it I think), probably there was a discussion on the old mailing list that could shed light on the situation here.

Maybe shadowing self was forbidden intentionally, and maybe for reasons like those stated here, or maybe it was a mistake. It's natural once something is the way it is to come up with justifications for not changing it. But if it wasn't intentional, I don't see why the compiler should be any stricter about self than other things.

I admit I've been annoyed about changing the name of the variable, mainly because then I can't bring up uses of the variable anymore by searching self. For me, keeping the name self when I rebind with pinning would be more useful for following the code than forbidding this. Others' mileage may vary.

EDIT: Like I would actually really prefer to be able to see the flow of calling Pin::get_unchecked_mut and then mutating the pinned value by just typing /self in vim. The fact that I can't trivially highlight this entire flow is actually a major annoyance to me when I'm trying to confirm the pinning validity of a poll method.

10 Likes

@cuviper could you elaborate on these patterns/statements? Perhaps it would make sense to just lift this restriction first inside methods and then look at other places on a case by case basis...