Stream: t-lang

Topic: capture clauses


view this post on Zulip nikomatsakis (Mar 12 2021 at 18:55):

So um I went a little nuts this morning and wrote up an argument in favor of an explicit capture clause annotation on closures:

https://hackmd.io/38jKhUkvSC2pkG1BrI3boQ?edit

Something like this is needed to complete the RFC 2229 work. This is more than we need, but I think it's readily implementable and would be really nice to have.

view this post on Zulip nikomatsakis (Mar 12 2021 at 18:55):

@T-lang :point_up: I'd like to hear loose thoughts, I can open up a project proposal -- in fact, I probably should've done that instead

view this post on Zulip Josh Triplett (Mar 12 2021 at 19:07):

Under RFC 2229, is there any way to write "capture the clone of an Arc rather than the original Arc" other than the block-with-let pattern or the proposed #[capture(x.clone())]?

view this post on Zulip Josh Triplett (Mar 12 2021 at 19:07):

Also, initial reaction, I feel like allowing expressions like x.clone() there is a rabbit hole.

view this post on Zulip Josh Triplett (Mar 12 2021 at 19:09):

I also don't think "prevent capturing anything that isn't listed" is a limitation we need to add to closures. (That's leaving aside other potential motivations.)

view this post on Zulip Josh Triplett (Mar 12 2021 at 19:09):

I do find the idea of explicitly saying "I want to capture this by reference" or "I want to capture this by value" interesting, but I wonder how often people will find the automatic detection problematic.

view this post on Zulip Josh Triplett (Mar 12 2021 at 19:09):

Other than edition migration, what are the use cases for "I know I could capture less, but I want to capture more"?

view this post on Zulip Taylor Cramer (Mar 12 2021 at 19:32):

Thanks for the detailed writeup! Overall, I'm not opposed to capture clauses. I'm curious how frequently you imagine users reaching for explicit capture clauses (e.g. is this something that we'd encourage folks to use most/all of the time, or is this an exceptional thing?).

Other unorganized thoughts:

x.0.clone() becoming named x will be a source of confusion, and I think I'd prefer x = x.0.clone(), even though it's a slight ergonomic hit. I also think I'm not yet convinced of the relative merit of

I'm not totally sold on the "why this can't be a procedural macro" motivation. Ensuring that no other variables are captured feels like an unnecessary step for users to me, perhaps because I'm used to today's Rust where variable captures are implicit.

view this post on Zulip Taylor Cramer (Mar 12 2021 at 19:42):

I do want <ident>.<ident>(..) to bind to the first identifier though, to allow for getters / as_ref/_mut. This relates to a separate concern I have that also applies to the new capture behavior. We currently have a big language pain point around setters/getters not having similar expressivity to field access. This gap stinks, as it imposes severe limits on the usage of structs with private fields. Method access to private fields is useful for guarding against future changes to types (e.g. moving a field behind a Box) and when representing lazily deserialized structures.

view this post on Zulip Taylor Cramer (Mar 12 2021 at 19:43):

I suppose one way around that is to only offer the <ident> = <expr> syntax, but that quickly becomes quite verbose to the point that an explicit block with let bindings isn't much less inconvenient.

view this post on Zulip Josh Triplett (Mar 12 2021 at 22:04):

I do think, other than "prevent anything else from being captured", none of this needs to be at the language level.

view this post on Zulip Mario Carneiro (Mar 12 2021 at 22:32):

I think this looks a lot better than the { let x = x.clone(); || ... } idiom (in particular, the default formatting is more convenient), although the attribute makes it clear that this is an afterthought-addition to the language. But it's rare enough that that doesn't bother me so much. For anything other than &x and &mut x, name shadowing should require rebinding like x = x.0 for consistency with patterns and struct literals. Something like x.clone() seems just a bit too magical for something in the language itself; it looks more like what a third party crate would do, where you can afford to go nuts with syntax sugar.

As for whether this should be a procedural macro, as a user I don't think it matters, unless this is outside std. I see a possible use for non-.. captures but I don't think it should be the default; the main reason I would reach for this feature is to edit a single binding that isn't working right, not ban all the others.

view this post on Zulip scottmcm (Mar 12 2021 at 23:06):

Random thought: if it only has explicit captures, it could expose those captures under their names as public fields.

(I'm not yet convinced this is a good idea, but I recall people asking about ways of getting things out of closures, which seems more justifiable if they had to say exactly what was in them anyway.)

view this post on Zulip scottmcm (Mar 12 2021 at 23:09):

On the naming thing: maybe we can lean on intuition from &self? So &x and &mut x would be enough, but for anything more complicated it's x = y.z.4.

(Though I'm not really a fan of expressions in attributes, since it makes me think it wants to be "real" syntax.)

view this post on Zulip isHavvy (Mar 13 2021 at 06:48):

One of the things I like in JavaScript is that I can refer to properties on the function object from the function.

function make_public_adder(addend) {
    let fn = function this_fn(b) {
      return this_fn.a +  b;
    };
    fn.a = addend;
   return fn;
}

view this post on Zulip Mario Carneiro (Mar 13 2021 at 07:12):

I think it would be much better to just stabilize FnOnce<T> so that people can write callable objects rather than adding so much sugar on closure types

view this post on Zulip nikomatsakis (Mar 13 2021 at 09:28):

Josh Triplett said:

I also don't think "prevent capturing anything that isn't listed" is a limitation we need to add to closures. (That's leaving aside other potential motivations.)

I should perhaps strengthen the motivation argument there -- in short, I think I would find it surprising to see

#[captures(a, &b)] || { .. }

and then find out that the closure also captures c. I can say that I personally have found it annoying sometimes to determine, if there is a long closure, exactly what it captures -- for this reason I personally tend to avoid long closures and prefer methods, so that I can see exactly what state is being passed, but others feel differently.

view this post on Zulip nikomatsakis (Mar 13 2021 at 09:30):

Josh Triplett said:

I do think, other than "prevent anything else from being captured", none of this needs to be at the language level.

Yes, agreed in a technical sense. But I think capture clauses will feel like part of the language however they're implemented, and we do need to ship it by default so we can use it in migration.

view this post on Zulip nikomatsakis (Mar 13 2021 at 09:31):

Taylor Cramer said:

x.0.clone() becoming named x will be a source of confusion, and I think I'd prefer x = x.0.clone(), even though it's a slight ergonomic hit.

Agreed. I wasn't sure about that part of it. I'd be happy to require an explicit name for cases that involve fields. I'm also not sure how important fields are, tbh, but I wanted some way to be "as expressive as the inferred cases", I think (which reminds me, we should include * in the list of places we accept). I should add those notes to the RFC.

view this post on Zulip nikomatsakis (Mar 13 2021 at 09:31):

scottmcm said:

Random thought: if it only has explicit captures, it could expose those captures under their names as public fields.

Plausible future extension, perhaps with pub keyword or something.

view this post on Zulip nikomatsakis (Mar 13 2021 at 10:42):

OK, I've reworked the draft to include the feedback. I've done the following:

view this post on Zulip nikomatsakis (Mar 13 2021 at 10:42):

I also added some more interesting potential future directions that came to me.

view this post on Zulip nikomatsakis (Mar 13 2021 at 10:43):

Mario Carneiro said:

I think it would be much better to just stabilize FnOnce<T> so that people can write callable objects rather than adding so much sugar on closure types

Can you elaborate a bit more? I don't know exactly what you mean. However, I also think this is unlikely to be something we could fully design and ship in time to be used with the RFC 2229 plans.

view this post on Zulip Daniel Henry-Mantilla (Mar 13 2021 at 11:53):

Ohhh I've wanted explicit closure captures for a while!! When programming with unsafe one needs to be very careful with what gets captured, for instance.
I am, however, skeptical of the attribute syntax: std / official attributes have never interacted with runtime code like that. What I mean is while it is possible to write Rust code inside an attribute parameter, only some proc-macros use that (some in a stringified fashion, others in a raw fashion), and code highlifhters handle that wrong.

To me, the better syntax would be to use the move annotation with some optional list of captures. After all, moves and captures mean the same thing, but for the unique "fake binding" sugar for by-ref and by-mut-ref captures. But precisely thanks to the ref and ref mut keywords, that wouldn't impact that often:

let x: (_, _) = ...;
let f = move(ref x) || stuff(&x.0);
//or
let f = move(x = &x) ...

view this post on Zulip Daniel Henry-Mantilla (Mar 13 2021 at 11:57):

The grammar being that it's either an identifier with optional ref and mut modifiers, or a pat = expr (could be a : to mimic struct sugar, but since that syntax was deemed problematic in hindsight, maybe we shouldn't use that in new places, and favor = in its stead)

view this post on Zulip Daniel Henry-Mantilla (Mar 13 2021 at 12:00):

This would also solve the awkwardness of

// why should this be mut?
let mut x = 42;
let mut f = move || { x += 1; x };

we'd have instead:

let x = 42;
let mut f = move(mut x) || ...;

view this post on Zulip Josh Triplett (Mar 13 2021 at 17:00):

I really like the idea of attaching this to the move keyword.

view this post on Zulip Josh Triplett (Mar 13 2021 at 17:02):

@Mario Carneiro Yes, I'd prefer stable callable objects as well, rather than the closure equivalent of field definitions.

view this post on Zulip Josh Triplett (Mar 13 2021 at 17:07):

@nikomatsakis My point was that if migrations don't need to limit what gets captured (just expand it), that doesn't need a new syntax, since it could use one of the several translations we'd already talked about (drop or a capture macro).

view this post on Zulip Josh Triplett (Mar 13 2021 at 17:08):

This syntax itself may potentially provide great value, and I'd love to see more use cases for it, but I don't think migration is necessarily one of them, given that.

view this post on Zulip Josh Triplett (Mar 13 2021 at 17:10):

I'm starting to see the use case you're getting at for explicitly listing the captures of a closure. That does seem potentially useful, to help wrangle complexity. Rather than tying it to migration and the edition, I'm wondering if we could instead develop that syntax orthogonally to migration, and without having to rush to meet the edition deadline.

view this post on Zulip Aaron Hill (Mar 13 2021 at 17:23):

I was actually thinking of making a proc-macro that would try to implement this (using some hygiene hacks to prevent accessing variables that aren't listed). I'd be strongly in favor of having this built-in to the language

view this post on Zulip Aaron Hill (Mar 13 2021 at 17:26):

As a future extension to this, it might be useful to have an allow-by-default lint that that lints against any closures not using the explicit capture syntax. This would allow projects to require that all of their code uses the explicit capture syntax. However, this would essentially be a clippy 'restriction lint' that's part of the main compiler, so I'm not sure that the policy around that is

view this post on Zulip bstrie (Mar 13 2021 at 18:28):

I really like the idea of attaching this to the move keyword.

I sort of do too. I remember being against keyword(modifiers) when pub(crate) first came out, but on reflection it seems like it solves a variety of syntactic problems, e.g. you could imagine the weird pseudo-lifetime syntax of labeled-break as instead while(label)/break(label).

although, in any case there's no reason this couldn't start life as an attribute and be expanded into syntax later.

view this post on Zulip RalfJ (Mar 13 2021 at 18:29):

Josh Triplett said:

I really like the idea of attaching this to the move keyword.

me, too -- with the = syntax it even supports x = x.clone().
looks like we're moving closer towards C++ closures then. ;)

view this post on Zulip Josh Triplett (Mar 13 2021 at 19:03):

@RalfJ That's actually one of the reasons I didn't care for it at first. I don't like C++'s much more manual closures and manual capture syntax.

view this post on Zulip RalfJ (Mar 13 2021 at 19:05):

oh sure, keep the automatic mode. but as was said by others above, sometimes this control is what you want.

view this post on Zulip pnkfelix (Mar 15 2021 at 19:52):

nikomatsakis said:

@T-lang :point_up: I'd like to hear loose thoughts, I can open up a project proposal -- in fact, I probably should've done that instead

I had thought there were (well-established?) issues with putting variables/expressions into attributes, in terms of how that interacts with hygiene and name-resolution. But maybe some of those issues are resolved when the attribute in question is still within the scope of the variable it refers to.

view this post on Zulip pnkfelix (Mar 15 2021 at 20:02):

Also, I’m curious how strong the motivation is to have this expressed as a single explicit list on the lambda expression (or, more generally, each lambda expression, when they are nested.) it would be good if the document considered the alternative of attaching this attribute (or some variation thereof) within the closure body. Something like let c = move || println!("{:?}", (#[capture] x).1); (to capture all of x) vs let c = move || println!("{:?}", #[capture] x.1); (which would capture just the field.

view this post on Zulip Mario Carneiro (Mar 15 2021 at 20:26):

that's definitely less expressive, since it doesn't cover the clone() examples, although it's probably sufficient for recovering expressivity loss from RFC 2229

view this post on Zulip nikomatsakis (Mar 15 2021 at 23:01):

OK, just catching up here

view this post on Zulip nikomatsakis (Mar 15 2021 at 23:02):

I'm debating about the move keyword -- my biggest concern is that it doesn't express the implicit capture case. I don't consider move(ref x) to be very nice, because it doesn't behave like ref does in patterns. A ref x pattern gives x a type of &T, but implicit capture in patterns gives x a type of T.

view this post on Zulip nikomatsakis (Mar 15 2021 at 23:03):

I think the line between attributes and regular syntax is pretty squishy; we have tried to hold the line of a 'attributes don't change the "intent" of the code' -- so e.g. you can add #[repr] attributes to a struct, and it may affect the layout, but for the most part you don't need to look at that to understand code using the struct.

view this post on Zulip nikomatsakis (Mar 15 2021 at 23:03):

One could argue the same is true of #[capures], but it's a bit trickier

view this post on Zulip nikomatsakis (Mar 15 2021 at 23:04):

Anyway I have to run now but later on I'll go over the thread and try to summarize the feedback and update the RFC

view this post on Zulip nikomatsakis (Mar 15 2021 at 23:04):

@pnkfelix a quite note -- I think that the main problem is that it fails to meet my motivation of 'easily identify what is captured by the closure'

view this post on Zulip nikomatsakis (Mar 15 2021 at 23:06):

I guess that one could say that .. in "move" means "implied mode", i.e., we will move these things, and infer the rest

view this post on Zulip nikomatsakis (Mar 15 2021 at 23:06):

after all, my #[captures] annotation is only moves

view this post on Zulip nikomatsakis (Mar 15 2021 at 23:07):

so that the explicit form becomes move(&x) || .., and the migrations insert move(a, ..) || { use(&a, &b) } (which would mean "capture a by move, and b by shared reference".

view this post on Zulip nikomatsakis (Mar 15 2021 at 23:07):

I don't know, that last example looks awfully misleading to me.

view this post on Zulip Josh Triplett (Mar 15 2021 at 23:28):

I definitely wouldn't want move(ref x); I think move(&x) logically means "move a reference to x into the closure".

view this post on Zulip pnkfelix (Mar 16 2021 at 00:06):

(I debated about whether to try to generalize my counter proposal to #[capture] (x.clone()), but at that point I think what one is really trying to express is some notion of Aspect Oriented Programming, ie injecting arbitrary code that is meant to be run right before the lambda expression is evaluated. I do think it’s nice to have the definition of the desired expression close to where it will be used (Ie nested within the lambda body) but I suspect it would also be pretty confusing, much like AOP )

view this post on Zulip nagisa (Mar 16 2021 at 00:10):

Right the question is what do you consider expressions inside a move() to be. To me they sound a lot like uses, much like passing things to a function as arguments, rather than patterns. So a strong vote for move(&val) from me in context of the move syntax.

view this post on Zulip nagisa (Mar 16 2021 at 00:14):

Incidentally I wrote some pretty similar code to what the misleading example looks like in C++ just now. I think the resulting code is probably somewhat worse in C++ as you can still mutate the referenced value after it has been captured. With rust's unique mutable ownership semantics that would be… less of a problem.

view this post on Zulip Mario Carneiro (Mar 16 2021 at 02:34):

Also it just occurred to me that in the move(a, b) || use(&a, &b) example there is a serious danger of the reader seeing two function calls separated by OR, if they don't know rust very well and don't realize move is a keyword (or if they just read too quickly). Hopefully they have good syntax highlighting. (That said, I don't really have a better syntax suggestion.)

view this post on Zulip nagisa (Mar 16 2021 at 10:17):

well other kinds of grouping tokens could be used. move[...], move<...>, move{ ... }. I'm quite partial to the last option as an alternative as it looks a lot like struct construction syntax which what closures really are

view this post on Zulip pnkfelix (Mar 16 2021 at 16:22):

Mario Carneiro said:

Also it just occurred to me that in the move(a, b) || use(&a, &b) example there is a serious danger of the reader seeing two function calls separated by OR, if they don't know rust very well and don't realize move is a keyword (or if they just read too quickly). Hopefully they have good syntax highlighting. (That said, I don't really have a better syntax suggestion.)

(this definitely concerned me as soon as I saw that example, because that’s how my brain parses it at first glance. I’d want to explore ways to make it clearer that this is not an or-expression. Not sure what would actually work, though.)

view this post on Zulip pnkfelix (Mar 16 2021 at 16:25):

(of options @nagisa posted, move<…> is one that strikes me as “most different” from an expression. but maybe move { … } would do…)

view this post on Zulip Daniel Henry-Mantilla (Mar 16 2021 at 18:55):

So regarding move, I don't mind the grouping sigil used there, I considered brackets given the C++ syntax, and braces given the parallel with the env struct that the closure would have there (but in that case we might go back to using : <expr>?).

And regarding being able to explicitly express the semantics of current implicit by-ref captures, is that a hard requirement? AFAIK, the only place where that occurs is with closures; no user-written code can mimic that, ever. That can be convenient when wanting to be implicit (e.g., imagine using the with api of LocalKey: one just writes codes without worrying about captures and it Just Works™; this is always the cause for immediately-called closures, and in that case, hiding the technicalities of closure captures makes a lot of sense. But if we are go to using move { a, b } captures, that is, when captures matter, I find the lack of sugar to be quite acceptable, since it consistently mimics pattern destructuring (e.g., in a hypothetical world, we could have made ref x still bind x to the value, but restricting the access to shared ones, like within a closure capture. In that world, it would make to also feature that capability for explicit closure captures, and the natural syntax for that would have been writing ref <binding>).

To summarize, I do think that this a / b "fields" ought to be written similarly to struct bodies:

let vars = Vars { x: 42 };
match vars { Vars { ref x, .. } => {
    let _: &i32 = x;
}}
let x = 42;
let f = move { ref x } {
    let _: &i32 = x;
};

as well as:

fn cartesian_product<'lt, L : 'lt + Copy, R : 'lt + Copy> (l: &'lt [L], r: &'lt [R])
  -> impl 'lt + Iterator<Item = (L, R)>
{
    //                                          consistent syntax
    //                                             vv    vv
    l.iter().flat_map(move |x| r.iter().map(move { &x } |&y| (x, y)))
}

For instance, if somebody that used to write let inc_ref = || Arc::clone(&x) wanted to write the capture explicitly, they would be able to write:

// Implicit by ref
let inc_ref = || Arc::clone(&x);
// Explicit
let inc_ref = move { ref x } || Arc::clone(x);
// or
let inc_ref = move { x: &x } || Arc::clone(x);

// Implicit owned
let inc_ref = move || Arc::clone(&x);
// Explicit
let inc_ref  = move { x } || Arc::clone(&x);

// Implicit owned clone
let inc_ref = { let x = x.clone(); move || Arc::clone(&x) };
// Explicit
let inc_ref  = move { x: x.clone() } || Arc::clone(&x);

The other alternative, but which, at the very least, should not be using braced syntax, is to get rid of patterns altogether (since, despite being a core part of the language, it is one very specific to Rust, so many programmers find this paradigm to be confusing and try to reduce it to a minimum (e.g. it is sometimes linted against, and it's also a reason motivating match ergonomics).

In that case, the syntax ought to be: non-brace group delimiter (I'd even say it ought to be using parenthesis here), and using = instead of :, so as to go as far as possible from struct patterns as possible:

let (x, y) =  (42, 27);
//                +------ values -------+
//                |                     |
//                vv                   vv
let f = (move(x = &x) |y| { /* … */ })(&y);
//            ^        ^
//            +- pats -+

And then add the (imho, bad) idea that &x would in this one instance of the language be a possible shorthand for a binding by ref: x=&x

//            +------- values ------+
//            |                     |
//            vv                   vv
let f = (move(&x) |y| { /* … */ })(&y);
//             ^   ^
//             bindings

view this post on Zulip pnkfelix (Mar 16 2021 at 19:05):

(e.g., in a hypothetical world, we could have made ref x still bind x to the value, but restricting the access to shared ones, like within a closure capture. […]

what an interesting idea. I wonder how that would have gone over; if it would have made ref “patterns" more palatable… (probably not).

view this post on Zulip nikomatsakis (Mar 16 2021 at 19:15):

And regarding being able to explicitly express the semantics of current implicit by-ref captures, is that a hard requirement?

The hard requirement is being able to migrate RFC 2229 closures in some way. We could use drop(&a); for that migration, but I would like there to be a syntax for us to move users to that is actually appealing, since in some cases annotations of this kind of will be necessary to get the desired semantics.

view this post on Zulip nikomatsakis (Mar 16 2021 at 19:15):

I suppose that we can migrate move closures using move(a, b, c) and non-move closures using drop(&a), but that seems ungreat.

view this post on Zulip nikomatsakis (Mar 16 2021 at 19:16):

We can support move(..) to mean "move any places used but not explicitly named here"

view this post on Zulip nikomatsakis (Mar 16 2021 at 19:16):

We could support move(ref..) or something for the current implicit capture syntax, but I don't love it, for the reasons I've given before (ref calls to mind ref bindings, and they operate differently)

view this post on Zulip nikomatsakis (Mar 16 2021 at 19:16):

also, some of the inferred captures may not be refs, they could be mut refs or moves

view this post on Zulip nikomatsakis (Mar 16 2021 at 19:17):

We could support move(a, b) and say that we have a lint that warns if you have any variables not cited in the move

view this post on Zulip nikomatsakis (Mar 16 2021 at 19:18):

nikomatsakis said:

The hard requirement is being able to migrate RFC 2229 closures in some way. We could use drop(&a); for that migration, but I would like there to be a syntax for us to move users to that is actually appealing, since in some cases annotations of this kind of will be necessary to get the desired semantics.

maybe we should go for this, I don't know-- we can certainly use drop(&a); for the migration and punt this, but it feels to me like a lost opportunity

view this post on Zulip Mara (Mar 16 2021 at 19:19):

instead of the rather cryptic drop(&a); drop(&b); drop(&c);, let _capture = (&a, &b, &c); would also work, right?

view this post on Zulip nikomatsakis (Mar 16 2021 at 19:24):

I added this FAQ:

view this post on Zulip nikomatsakis (Mar 16 2021 at 19:24):

Why not use move(CC) as the syntax instead of #[captures(CC)]?

Syntax like move(a, b, c) || a + b * c has certain advantages:

It has some disadvantages as well:

There are some other alternatives:

view this post on Zulip Daniel Henry-Mantilla (Mar 16 2021 at 19:24):

Regarding the migration, @nikomatsakis is this because it would be done by humans? Because an automatic rustfix would be able to prepend * to all the occurrences of a binding (x -> (*x)):

// old:
let f = || println!("{}", x);
// new:
let f = move(ref x) || println!("{}", *x);

view this post on Zulip nikomatsakis (Mar 16 2021 at 19:25):

It is done by rustfix, but that's a lot more code to write

view this post on Zulip nikomatsakis (Mar 16 2021 at 19:25):

I suppose it is plausible

view this post on Zulip nikomatsakis (Mar 16 2021 at 19:25):

move(&x) || println!("{}", *x) <-- it would probably be that ;)

view this post on Zulip nikomatsakis (Mar 16 2021 at 19:25):

Mara said:

instead of the rather cryptic drop(&a); drop(&b); drop(&c);, let _capture = (&a, &b, &c); would also work, right?

that's true, I consider this also rather cryptic though

view this post on Zulip nikomatsakis (Mar 16 2021 at 19:25):

drop((&a, &b, &c)); is more compact, but equally cryptic

view this post on Zulip nikomatsakis (Mar 16 2021 at 19:26):

nikomatsakis said:

I suppose it is plausible

this may be worth considering

view this post on Zulip Mara (Mar 16 2021 at 19:26):

yeah, all options other than new syntax/attributes are cryptic. but it'd be nice to avoid the word drop. also, clippy will get angry at you for dropping Copy stuff like references.

view this post on Zulip nikomatsakis (Mar 16 2021 at 19:27):

I suppose we could say that rustfix adds drop(&a) and people migrate by hand

view this post on Zulip nikomatsakis (Mar 16 2021 at 19:27):

also

view this post on Zulip Daniel Henry-Mantilla (Mar 16 2021 at 19:27):

(fwiw, I personally find let _captures = to be a bit more readable than drop)

view this post on Zulip nikomatsakis (Mar 16 2021 at 19:27):

like, the move(&a) is the recommended way to do it, but...

view this post on Zulip nikomatsakis (Mar 16 2021 at 19:27):

Daniel Henry-Mantilla said:

(fwiw, I personally find let _captures = to be a bit more readable than drop)

it is, it's just not good enough (to me) to be the "official' way to do this

view this post on Zulip nikomatsakis (Mar 16 2021 at 19:27):

and i think we want something to tell authors of new code when they're like "how can I force it to capture all of a"

view this post on Zulip Daniel Henry-Mantilla (Mar 16 2021 at 19:27):

That's fair

view this post on Zulip nikomatsakis (Mar 16 2021 at 19:27):

I'd rather that not be let _capture = &a

view this post on Zulip nikomatsakis (Mar 16 2021 at 19:28):

the "cliff" to move is a bit unfortunate but maybe ok

view this post on Zulip Mara (Mar 16 2021 at 19:28):

(sure. but let _ = &a; is still better than drop(&a); which keeps coming up as an example)

view this post on Zulip Mara (Mar 16 2021 at 19:29):

looking forward to explicit capture syntax

view this post on Zulip nikomatsakis (Mar 16 2021 at 19:30):

I suppose let _ = &a actually does work

view this post on Zulip nikomatsakis (Mar 16 2021 at 19:30):

I thought it didn't, beacuse of the "special semantics" of let _ = <place>

view this post on Zulip nikomatsakis (Mar 16 2021 at 19:30):

but &a is not a place

view this post on Zulip nikomatsakis (Mar 16 2021 at 19:30):

regardless, the point stands :)

view this post on Zulip nikomatsakis (Mar 16 2021 at 19:31):

I do agree let _ = (&a, &b, ... &z); is a better migration

view this post on Zulip nikomatsakis (Mar 16 2021 at 19:31):

I just don't love it as the official answer for what to do

view this post on Zulip bstrie (Mar 16 2021 at 19:31):

ballpark estimate of how common you expect this to be needed? 1% of the time, more, less? using an attribute as placeholder syntax isn't a bad idea if it's a particularly rare case, as long as you're okay with attributes referencing runtime things

view this post on Zulip nikomatsakis (Mar 16 2021 at 19:33):

Fairly uncommon

view this post on Zulip nikomatsakis (Mar 16 2021 at 19:34):

I think if it were just rfc2229 I wouldn't worry that much, though I'm not thrilled, but I think that if you add up the various motivations, you get a stronger case.

view this post on Zulip nikomatsakis (Mar 16 2021 at 19:34):

I'm going to weigh the move syntax idea a bit :)

view this post on Zulip Daniel Henry-Mantilla (Mar 16 2021 at 19:37):

Potentially stupid suggestion I just came up with: if we are not using ref x but &x, we no longer have to follow pattern logic at all, so what about move(&x) captures by reference but binds to the type of x?

view this post on Zulip Daniel Henry-Mantilla (Mar 16 2021 at 19:38):

let x = 42;
let get_ft = move[&x] || -> i32 { x };

view this post on Zulip Daniel Henry-Mantilla (Mar 16 2021 at 19:40):

image.png

view this post on Zulip Mara (Mar 16 2021 at 19:41):

not ref x, but still mut x?

view this post on Zulip Daniel Henry-Mantilla (Mar 16 2021 at 19:45):

:thinking: Allowing the move[mut x] shorthand would be convenient but inconsistent with the &x suggestion I just made.
I guess the mut case ought to need the full syntax (which does not seem too bad to me):

move[mut x = x] || { /* … */ }

view this post on Zulip Mara (Mar 16 2021 at 20:04):

then you could also do move[mut x = 0] || { x += 1; x } without needing to declare an x outside of it at all. but at this point move is a bit of a weird word to use here.

view this post on Zulip bstrie (Mar 16 2021 at 20:09):

what about move inside the closure? || { move x; ... }

view this post on Zulip cuviper (Mar 16 2021 at 20:11):

we've had other discussions about move in expressions, not directly related to closures

view this post on Zulip cuviper (Mar 16 2021 at 20:11):

https://rust-lang.zulipchat.com/#narrow/stream/122651-general/topic/In.20a.20closure.2C.20how.20do.20I.20move.20some.20things.20but.20not.20others.3F

view this post on Zulip cuviper (Mar 16 2021 at 20:13):

well I guess that did start about closures, but the .move idea has potential traction regardless, IMO

view this post on Zulip cuviper (Mar 16 2021 at 20:14):

here's another thread on that:

view this post on Zulip cuviper (Mar 16 2021 at 20:14):

https://rust-lang.zulipchat.com/#narrow/stream/213817-t-lang/topic/Spitballing.3A.20a.20.60move(x).60.20expression.3F

view this post on Zulip Daniel Henry-Mantilla (Mar 16 2021 at 20:19):

Mara said:

at this point move is a bit of a weird word to use here.

Interesting, I personally find it very intuitive: we are moving a value (0) as x into the closure('s captures). The one a bit weird is my suggested semantics for move(&x), since it would be kind of an oxymoron :sweat_smile: (in that regard, #[captures] has at least the advantage of being a bit more readable)

view this post on Zulip bstrie (Mar 16 2021 at 20:22):

yes, that thread on move expressions seems pretty relevant here, although if the semantics are chosen such that let y = x.move invalidates x even if x is Copy, then for the purposes here you might sometimes need to do (&x).move (or move &x), which I suppose is no worse than the drop or let _captures forms above


Last updated: Sep 26 2021 at 12:03 UTC