Just spitballing here but, what if you could unshadow names?
There are many ways to have lexical unscoping. Currently Rust has one:
{
let x = ...;
}
// no x
But what if we had more? Something like this:
let move x = ...;
let y = move || x;
// no x
could be convenient with clone:
let x = Arc::new(());
let move x = x.clone();
let y = move || x; // maybe even ditch the `move` here entirely?
let z = move || x;
or, if willing to ignore/overlook guaranteed soundness issues with existing stack-pin macros:
let x = Arc::new(());
let x = x.clone();
let y = move || x;
del x; // bikeshed the keyword name
let z = move || x;
let x = some_unpin;
pin_mut!(x);
del x;
let y = x; // eep!
but anyway just, thoughts on non-hierarchical lexical scopes?
With del, you could also do stuff like this, where you temporarily unshadow it inside an inner scope:
let x = foo;
let x = bar;
{
del x;
x.foo();
}
x.bar();
(note that let move wouldn't be able to do this one, unfortunately... or fortunately.)
Nah, let move is pretty tame. The discussion about del is mostly... for context. We mean, that was our original idea back on the Lua mailing list, and let move is inspired by that.
Fwiw, the only issue with the existing pin_mut! is that it does:
let mut $x = $x;
let $x = unsafe { ... $x };
and would be fine if it were:
let mut storage = $x;
let $x = unsafe { ... storage };
I think this is a bad idea because it would be unclear to the programmer which binding is being used. The to-be-allowed code currently has a meaning – to use the moved-from binding – and this would change that existing stable behavior. The existing meaning (deliberately) doesn't compile, but the code still already has a clear and obvious meaning.
The advantage of this is purely in a fixed, small reduction in the number of unique names a programmer has to write in a program, at the expense of then later reading the same program.
Note that it is possible to use a shadowed binding via hygiene, if you were able to use it previously:
let x = 0;
macro_rules! m { () => (x); }
let x = ();
dbg!(m!()); // prints 0
It is a common pattern (at least we find ourselves using it often) to do
{
let x = x.clone();
foo(move || x);
}
We don't see what's confusing about
let move x = x.clone();
foo(move || x);
A reduction in indentation doesn't seem like a big deal but it's really easy to overdo indentation when using fairly simple macros, like impl_trait! from the impl_trait crate. Mostly because it already forces 2 indents on you, compared to the "conventional" way of implementing traits: one for the impl_trait! macro itself, and one for the impl trait block inside the inherent impl. This is compounded by us following the 80 columns rule, 16 (20%) of which are just to get into the function itself in impl_trait!:
We especially noticed this being an issue with the hexchat-plugin crate. But anyway, we digress, the main point is that the ability to move a whole binding into a closure seems more ergonomic to us?
It sounds like the principal motivation is getting rid of temporaries consumed by moves into closures. Can't you write this instead?
foo({
let x = x.clone();
move || x
})
If that's still too much indentation (maybe the typical closure body is a lot more complicated than the example suggests?) you could hide it in a macro:
foo(move_binding!(x = x.clone(), || x))
And that suggests to me the purely syntactic extension
foo(move |x = x.clone()| x)
which would desugar to the same thing as the macro expansion.
pin_mut! already has to move the to be pinned value before pinning it as macros can refer to the shadowed variable if they are defined before the shadowing location.
let foo = 0;
macro_rules! get_foo { () => { foo } }
let foo = 1;
assert_eq!(get_foo!(), 0);
Yes, but pin_mut! sadly introduces two new bindings to the current scope, both with the same name.
It doesn't rely on hygiene, but on the assumption that nobody would ever suggest introducing unshadowing/non-hierarchical lexical scopes to Rust. (movable whole bindings would still be sound, tho.)
I had no idea this was possible - it seems pretty concerning to me.
I would have thought that both foo variable definitions should have the same hygiene, which should make the first one completely inaccessible after it's shadowed
Isn't the whole point of macro hygiene to require that symbols used in the macro be unaffected by code in the invoking scope? What other behavior could you have? If you wanted a macro to refer to a variable in the invoking scope, you have to pass the symbol in. Otherwise it would behave like a C macro.
Both definitions of x are present in the same lexical scope, and the macro body isn't actually parsed until it's invoked. I would have thought that the def-site hygiene used for the token x (it's being used as a local variable, so the mixed-site hygiene turns into def-site hygiene) would only take the lexical scope of the macro definition into account. However, it appears to also consider the location of the macro definition within the lexical scope.
Ah, I see the problem then. The macro definition isn't acting like an item, but instead like a statement.
Though you could argue that variable shadowing implies the existence of an implicitly narrower scopes, so the idea of there being one lexical scope here doesn't sit right; there are two foos, so there are two lexical scopes. Actually, each let statement starts a new child scope with new variables, so that code is basically equivalent to:
Movable bindings are moved into the next inner context they're used in.
let move x = true;
let y = x && x; // moves x into a temporary context
// no more x in this scope
let move x = foo;
let y = &x; // error about temporaries
// no more x in this scope
let move x = foo;
let y = x; // moves the binding into the expression, the value into y
// no more x in this scope
let move x = foo;
let y = || x; // moves the binding into the closure
// no more x in this scope
But how does this interact with nested contexts?
let move x = foo;
let y = || {
let z = || x; // does x get moved into this closure?
// can x be used here?
};
// no more x in this scope
Also:
let move x = foo;
let y = {
let z = || x; // does x get moved into this closure?
// can x be used here?
};
// no more x in this scope
There are 2 rules available here: "move into inner context only" and "move into used context". Neither have great ergonomics, but "move into used context" is more flexible, because you can rebind it:
let move x = foo;
{
let x = x;
{
x...;
}
// x is still available here
}
// no more x in this context
If the binding gets moved into a closure, the value does too.
let move x = foo;
let y = || {
// x is in this context
let z = || { x };
// no x in this context, let z consumed x.
}; // y is an FnOnce
Oh hmm wait this doesn't work... The problem with moving it into the context where it's used is that temporaries also count. The problem with moving it into the context where it's moved is that you'd have to use move closures anyway to move the value into the closure, but then you'd lose the "move" tag on the binding? And it wouldn't work with non-move closures.
We really want this idea to work, but we're not sure how to make it work in a way that's simple to understand and teach.
let move mut x = String::new()
x.push_str("hello "); // borrows x
x.push_str("world"); // borrows x
let y = || x; // moves the whole binding
It does mean move bindings get special-cased by closures, however, we think that'd be okay, because part of the point is to have finer control over move captures.
let state = State::new(...); // interior mutability
let move mut data = state.get_data(...);
let closure = |...| { data.update(...); state.set_data(..., data); }
(yeah, we know how to do this with current rust. yet we still think there are significant ergonomic benefits to be explored here.)
So: we don't like move closures, in general. And we don't think moving the whole binding is harmful. But it needs to be done in a way that makes sense. But thanks to that thread we feel like we can come up with an even simpler set of rules: move just makes closures move the binding where they'd otherwise borrow it.
if there are no closures or the move is implicit, the compiler warns of unnecessary move. e.g.:
let move x = foo; // warning: unused `move`
let y = x;
or
let move x = foo; // warning: unused `move`
let y = || drop(x);
as a bonus, these semantics can be implemented without introducing movable bindings!
let move x = foo;
let y = || x.foo(); // value moved here
x // error: use of moved value
this could then be changed to move the whole binding in the future. but it would solve some of the annoyances we personally have with move closures. ^^
we think with movable bindings, the tricky part is e.g.:
let x = 1;
let move x = foo;
let y = || {
let move x = x;
let z = || x.foo();
// what is `x` here?
}
// obviously `x` here is `let x = 1;`
because it only moves into the next closure, so you need to move it again. with del x this wouldn't be an issue but that one causes issues with pin_mut!due to a bug in pin_mut where it introduces the backing binding into the user's scope.