In my previous comment, I looked at the "dot syntax" options and tried to discuss them. In this comment, I want to try and consider "the future". Again, I apologize for the length. If you prefer, this comment is also available on HackMd.
I've seen a few folks make an argument that roughly says:
We should just pick
await foo
for now -- then we might consider various generalizations going forward.
The reasoning is that there are a number of options that generalize await foo
to permit smoother interaction with ?
:
await?
-like syntaxfoo.await
(and potentiallyfoo.match
)- a "pipeline operator" that could be applied to
await
(e.g.,foo |> await
orfoo@await
).
I definitely understand the desire to future-proof. But I am also wary of underweighting the present -- we have a lot of experience that shows that await foo
is a poor fit for Rust's error handling strategy, for reasons that have been well documented by now.
In general, I think when we make decisions, we should consider the future, but also consider the very real possibility that a "temporary" solution winds up lasting a long time (or forever). This could easily mean that approximately 50% of the time, when people use await, they have to do something like (await foo)?
.
Are parentheses a big deal? Of course, some people don't see that as a problem. And certainly many people accustomed to JS have told me that it's "not considered a big deal" to write (await foo.bar()).baz()
. But I don't think that's the right analogy: I think the key point is that JavaScript uses exceptions for error-handling, and we don't, and that makes this a much bigger deal in Rust (and a much more frequent occurrence). I've found that using ?
is so much nicer than using try!(..)
ever was, and a significant part of that is that I do not have to "go back" and insert the try!(
at the start of the line. Porting sync code to async will be much more painful if we have to go put (
at the start of the line. Is it going to make or break Rust? Of course not. But I think it's "distinctly suboptimal".
Future-proofing is not free. "Keeping your options open" sounds great, but it comes with real costs:
- First of all, we have to spend time discussing and debating this design, instead of the polishing the implementation, or making streams work, or generators. (Or things entirely unrelated to futures -- this discussion has wound up "sucking in" folks like me, who would otherwise be hacking on other things.)
- Second, if we make any change here, we're going to have to update documentation, books, stackoverflow answers, and so forth. It would be much easier if we can just write those docs once.
- Finally, future proofing means that we are effectively locking ourselves into having "two ways to do it" in the future (unless we wind up sticking with the current choice, which I think is itself a problem). I am not convinced yet that we want two options.
- Granted, one could imagine deprecating the "temporary choice" after a permanent one is found. But that implies even more transition costs.
How likely are we to wind up with .await
anyway? I think this is a key question. At the end of this exploration, how likely is it that we will wind up with .await
? If we will, then it seems better to just "cut to the chase"
Ultimately, I give it fairly high odds. There are basically two competitors: await?
and a generalized pipeline operator. Let's look at them.
await?
is distinctly disadvantaged by not interoperating with map_err
and other such methods. Yes, you can implement those helpers for future types (though there may be coherence problems with that?), but at that point it feels like we're working awfully hard just to avoid postfix await. (I am not that worried about the "strangeness budget" here. The gap between await foo
and foo.await
is not that far, and there are clear reasons that the latter is a better fit for Rust.)
So what about pipelines? That's a bit harder to predict. But one thing I know for sure: I don't see us adding a generalized pipeline operator in the near term. That's a big syntactic bet for Rust, and I don't think we're at the point where that's a good strategy. So if we block on pipelines, I think that means we'll be using the "temporary" syntax likely for some time.
Worst case. So what's the worst case? If we adopt .await
now and then decide we want to adopt a different "pipeline operator" in the future, what happens? I see a few possibilities:
- maybe we find a way to build on the
.
operator, in which case everything is fine; - we could deprecate
foo.await
and addfoo#await
, despite the downsides; - we can just live with it
- after all, method chains like
foo.into_iter().map()
are here to stay; is.await
or.await()
so different? - in any case, it's not obvious why a pipeline operator needs to apply to keywords anyway. After all, a pipeline expression
a # b
takes two valuesa
andb
and is equivalent tob(a)
, right? Butawait
is not a value, it's an operator.
- after all, method chains like
The TL;DR
I am sympathetic to arguments that we should "future proof", but I'm also cognizant of the costs it brings. When I look forward, I feel like some form of "dot postfix" syntax is the most likely winner. I am inclined to just make the decision and be done with it.
If we were going to future proof, I think I would be inclined to go with the await!(...)
macro, and not the await foo
syntax. This is because it is clearly a transition measure, and I think we're less likely to get stuck in a "local maximum".
I'm going to keep thinking about this one, though. I'm definitely curious to hear if people think I got something wrong in my analysis.