--- title: "Case for moving `move` on async closures" author: TC date: 2024-11-02 url: https://hackmd.io/iWBQoottRwaihkjdkVwdpg --- # Case for moving `move` on async closures ## Intro In revising the upcoming stabilization report for async closures with CE, I've found myself regretting where we placed the `move` in the RFC. That is, the question is which of these we write: ```rust let _ = move async |_: u8| { .. }; let _ = async move |_: u8| { .. }; ``` I want to run by the team the case for adopting the first rather than the second as part of this stabilization to see whether we might have a quick consensus to change. This syntax question might have been eclipsed by the bounds syntax question. Resolving that, at least for me, made this one more prominent. If we don't have a quick consensus here, doing what's in the RFC is fine. It's better to get async closures stabilized. ## `async ||` should be an atomic unit The key semantic idea of async closures is to fuse together the state of the future and the closure. That is, async closures make the future and the closure "one thing". So in that context, I've come to see... ```rust let _ = async move |_: u8| { .. }; // ~~~~ // Interleaved mode switching. ``` ...as unnecessarily interleaved mode switching. The `move` is modifying an atomic `async ||`. It doesn't make sense, mechanically, to think about this as `async` modifying a `move ||`. As we later add other keywords, this just gets worse, e.g.: ```rust let _ = async gen move |_: u8| { .. }; // ~~~~ ``` Here, the `async gen ||` will be the atomic unit, and again we'd be breaking it up. ## Why we did it the other way We did it the other way because we write, today: ```rust let _ = async move { .. }; let _ = move || { .. }; ``` So it seemed OK to us to kind of squish them together and keep that order. In fact, I vaguely recall pushing in that direction myself. ## Why we could still fix this now We're combining two things, so there is no existing order set in stone. We have no existing precedent for breaking up `K || { .. }` by putting a modifier between the `K` and the `||`. If we act now, we can avoid having ever done that. The purpose of experimenting with things in nightly is to build experience and a "feel" for things. It's this experience that is pushing me to raise this for consideration. ## Looking toward `use(..)` We have on the table designs we're considering for the precise capturing of closure captures. In these designs, the `use` essentially replaces the `move`, and it would seem to make sense to allow `use` positionally where we allow `move`. E.g.: ```rust let x = String::from("x"); let y = String::from("y"); let _ = use(ref x, move y) || { .. }; ``` To my eye, this makes the mode switching problem worse. Compare: ```rust let _ = use(ref x, move y) async gen || { .. }; let _ = async gen use(ref x, move y) || { .. }; ``` ## Reflections on `use<..>` precise capturing This all reminds me of the reasons why, on precise capturing, we were strongly leaning toward putting `use<..>` first, e.g.: ```rust fn f<'a, T>() -> use<'a, T> impl Trait { .. } ``` Rather than... ```rust fn f<'a, T>() -> impl use<'a, T> Trait { .. } ``` ...before we decided to put it in the bounds. ## Conclusion I think, on the basis of having learned things during the nightly experimentation with this feature, that we should put `move` first when stabilizing async closures. If we agree, then great, we'll switch this in the impl and in the pending stabilization report (and describe the reasons for this change). If not, that's OK too, and we'll stabilize it as planned. What do we think (vibe check)?: | - | `move async \|\| { .. }` | `async move \|\| { .. }` | |--------------|--------------------------|--------------------------| | nikomatsakis | | | | tmandry | | | | Josh | -1 | +1 | | pnkfelix | | | | scottmcm | | | | TC | +1 | +0 | ## Appendix A: Looking toward ascription Separately from the reasons above, I do think we could later consider allowing `move` before `async` generally (and then pushing things that direction with e.g. an edition migration). There's a consistency here, as `async { .. }` blocks are actually closure-like, e.g., in that they are evaluated lazily and support `return`. So it's a bit odd to have `async move { .. }` but `move || { .. }` given this. As we look toward some ascription syntax, this gets more acute, and I think this is true whether that ascription syntax is `async -> T { .. }` or `async<T> { .. }`. In fact, it might be more true for `async<T> { .. }`. Consider this possible consistent arrangement: ```rust let _ = move || -> u8 { .. }; let _ = move async || -> u8 { .. }; let _ = move async -> u8 { .. }; // Or... let _ = move || -> u8 { .. }; let _ = move async || -> u8 { .. }; let _ = move async<u8> { .. }; ``` Versus: ```rust let _ = move || -> u8 { .. }; let _ = async move || -> u8 { .. }; let _ = async move -> u8 { .. }; // Or... let _ = move || -> u8 { .. }; let _ = async move || -> u8 { .. }; let _ = async<u8> move { .. }; ``` Again, as we stack more keywords, this makes the mode switching worse, e.g.: ```rust let _ = move || -> u8 { .. }; let _ = try async gen move || yield u8 yeet () { .. }; let _ = try async gen move yield u8 yeet () { .. }; // Or... let _ = move || -> u8 { .. }; let _ = try async gen move || yield u8 yeet () { .. }; let _ = try async gen<Yield=u8, Yeet=()> move || { .. }; let _ = try<Yeet=()> async gen<Yield=u8> move || { .. }; let _ = try async gen<Yield=u8, Yeet=()> move { .. }; let _ = try<Yeet=()> async gen<Yield=u8> move { .. }; ``` Of course, we could decide to ship `async move || { .. }` now and then allow for the `move` to come first if we later decide to allow that for `move async { .. }`. That's certainly an option. It just seems there might be reasons we could separate this case so as to "stop digging".