RFC #3710 describes what nikomatsakis believes to be the consensus we reached as a team in our meetup. The goal of this meeting is to review the scope of that document and in particular the concerns raised on the thread. Assuming the team remains aligned nikomatsakis plans to propose fcp merge
.
RFC #3710 defines the "flavor" design pattern. This is a syntactic design pattern and not a language feature. The RFC does not describe or commit to any form of "flavor generics" but it is intended to leave space for the possibility of such a feature. What follows is the "summary" section of the RFC.
This RFC unblock the stabilization of async closures by committing to K $Trait
(where K
is some keyword like async
or const
) as a pattern that we will use going forward to define a "K-variant of Trait
". This commitment is made as part of committing to a larger syntactic design pattern called the flavor pattern. The flavor pattern is "advisory". It details all the parts that a "flavor-like keyword" should have and suggests specific syntax that should be used, but it is not itself a language feature.
In the flavor pattern, each flavor is tied to a specific keyword K
. Flavors share the "infectious property": code with flavor K
interacts naturally with other code with flavor K
but only interacts in limited ways with code without the flavor K
. Every flavor keyword K
should support at least the following:
K
-functions using the syntax K fn $name() -> $ty
;K
-blocks using the syntax K { $expr }
(and potentially K move { $expr }
);K
-traits using the syntax K $Trait
;
K
-flavored traits should offer at least the same methods, associated types, and other trait items as the unflavored trait (there may be additional items specific to the K
-flavor). Some of the items will be K
-flavored, but not necessarily all of them.K
-closures using the syntax K [move] |$args| $expr
to define a K-closure;
K Fn
traits.Some flavors rewrite code so that it executes differently (e.g., async
). These are called rewrite flavors. Each such flavor should have the following:
🏠K<$ty>
defining the K
-type, the type that results from a K
-block, K
-function, or K
-closure whose body has type $ty
.
🏠K<$ty>
is a placeholder. We expect a future RFC to define the actual syntax. (The 🏠 emoji is meant to symbolize a "bikeshed".)cK
-block, consumes a 🏠K<$ty>
and produces a $ty
value.K
-function can be transformed to a regular function with a K
-flavored return type and body.
K fn $name($args) -> $ty { $expr }
fn $name($args) -> 🏠K<$ty> { K { $expr } }
The Future Possibilities discusses other changes we could make to make existing and planned flavors fit the pattern better. Examples of things that this RFC does NOT specify (but which early readers thought it might):
async
can be used with traits beyond the Fn
traits:
Read
trait, it will be referred to as async Read
, but the RFC does not specify whether to add such a trait nor how such a trait would be defined or what its contents would be.const Trait
ought to work (under active exploration):
const
-flavored trait should be const Trait
; it does not specify what a const
-flavored trait would mean or when that syntax can be used.🏠K<$ty>
:
async
, but the precise syntax still needs to be pinned down. RFC #3628 contains one possibility.The primary topics on the discussion thread have been as follows.
There was initially a misunderstanding that the RFC is proposing that async $TraitName
be derived via some mechanical or automated means. In fact the RFC does not say that, it only specifies that the K
-flavor of trait have a superset of the members of the unflavored version of the trait, not how the. From the FAQ:
That is not yet clear. The only flavored trait currently RFC'd is the
async
flavor of theFn
trait. It is not clear whether we will ever add anasync
flavor for theRead
trait or what language mechanism we would use to do so. It could be automatically derived fromRead
but it could also be a divergent definition.However, the RFC does give some guidance for flavored traits. Specifically, it says that a flavored trait should offer at least the same members as the unflavored version, some of which may themselves now be flavored. The
async FnOnce
trait is a good example of this. Like the ordinaryFnOnce
trait, it offers acall_once
method (but flavoredasync
) and anOutput
associated type. As an implementation detail, it has additional members, like theCallOnceFuture
; these are not currently exposed to users but they could be in the future.The reason that we specify that flavored traits have at least the same members as unflavored ones is to preserve the invariant that unflavored code can be ported to a flavor by "just adding the flavor keyword". If there is existing unflavored code using the trait, you should be able to "flavor" it easily. This would not be possible if the flavored version of the trait lacked some of the features or mechanisms of the original (unless of course the existence of those features is directly in contradiction with the purpose of the flavor, e.g., a panic-free flavor would not include a panic-free flavored method that is guaranteed to panic).
We then discussed a hypothetical alternative in which users could explicitly declare the "async flavor" of a trait, perhaps with a syntax like:;
trait async Fn { }
This led to some questions about how such a flavor would be imported. This is obviously not part of the RFC, but my response was that I would assume that users would continue to import Fn
(the actual identifier here). In other words, a trait async X
declaration is not declaring a separate trait X
but rather the async
-flavor for the trait X
. Like an impl
, this is not something one can import, but rather a connection made by the type system.
async Read
implies a "primary" read trait for asyncThe RFC only commits us to adding an async
flavor of the Fn
trait, but it does state that any future "async-flavored" traits will be referred to async TraitName
and not (e.g.) AsyncTraitName
. The point has been raised that this syntax, no matter its semantics, already implies that there will be some form of primary "async version of Read", and that this is not a good idea. GoldsteinE put it well in this comment:
Special syntax for flavours makes sense when there’s an obvious one-to-one correspondence between flavoured and unflavoured variants. That’s true for
const
andunsafe
, but that’s not true forasync
. By commiting to givingAsyncRead
a special syntax, we commit to choosing one of the flavoured variants and implicitly encouraging it at expense of the other. That’s a drawback of having a special syntax forAsyncRead
and any other trait that can have multiple incompatible definitions, which is not mentioned or in any way explored in the RFC text.
The following is a draft FAQ not yet present in the RFC:
The RFC does not commit to async Read
. There are good arguments in favor of having an async-flavor of Read
, but (as the question indicates) there are also some counterarguments. While this RFC does not settle the question, it does narrow the options somewhat to the following:
async Read
trait. The trait must have a superset of Read
's functionality but with some members made async
. In this scenario, nothing precludes us from adding other traits to target completion-based scenarios, they simply need to have a different name.
Read
" trait that closely follows sync read and two subtraits, ReadyRead
and OwnedRead
, that allow for being more specific. Note that nrc's async-flavored read also includes methods like as_ready
not found in the sync Read
. We would have to decide whether to make distinct sync-and-async flavors of ReadyRead
and OwnedRead
or to have those traits be async-only.Read
but instead add async-only traits with different names (e.g., BorrowedRead
and CompletionRead
). We can comment on Read
that there is no async flavor of Read
because, when writing async code, one really ought to pick between the two varieties.Choosing between these two options can be done in the future. We should take into account "clarity of purpose" and "correctness by construction" Rust design goals when doing so.
Note the expectation that not every trait will have an async flavor. Many traits will just have the "default" flavor and that flavor may include async methods.
async
and const
The premise of generalizing async Fn
to apply to other traits like async Read
is that people will be able to move code from sync-to-async by just adding async
. As a variation on the previous point, PeterHatch points out that this may be true in simple cases but that the reality is more complex:
I think that story is only simpler at that high a level of abstraction. If you know what the right places are, you know why they need to change. If you know you want a different version of a trait, changing the name is what you do every other time that's what you need, so that seems simpler to me. If you don't know you want a different version of a trait, just adding a keyword means you still may not understand what the change did, where changing the name is extremely clear.
Also, even if that's the story we want, it's not going to be the case for any traits outside the standard library. Consistency across the ecosystem seems important; if we eventually give the rest of the ecosystem the tools to change and encourage them to do so, we can change the standard library then as well.
No response has been drafted yet because I only saw this right now!
Other concerns about async Fn
as a syntax:
The RFC originally used the term "color". @clarfonthey expressed a preference for the term "effect" or potentially "affect", arguing that this is what the term has precedent both amongst current Rust WGs and academic material. [1, 2, 3].
The FAQ outlines the following criteria for selecting a name:
- Memorable and specific: A term that is too common and generic can be confusing. This can be because the word is used too much for too many things or because the word is just such a common English word that it doesn't sound like a specific thing.
- Grammatically flexible: It's good to have a term that can be used in multiple ways, for example as an adjective, noun, etc.
- Approachable: When I say "jargon" what I often mean is that it's a word that kind of has a formidable feeling, like "monad". When you hear the term you instantly feel a little bit excluded and less smart (or, alternatively, when you know the term well and use it correctly, you feel a bit smarter). That seems like a problem to me.
- Familiar: it's good to use a term that people find familiar and that gives them the right intuitions, even if it's not aligned on every detail.
Based on this criteria, I opted for the "flavor" pattern. This name is not binding in that it doesn't correspond to anything we expect to use in the language as of yet, but it is likely to appear in documentation or explanatory material, so it's worth thinking seriously about.
Other alternatives considered (from the FAQ):
Effect is reasonably memorable and specific but it not familiar nor approachable to people in conversation. It is also not grammatically flexible. For example, the RFC frequently talks about
K
-flavored traits, but it's not clear what the adjective form of an effect is (K
-effected traits?).Color was initially chosen as a playful allusion to the "What color is your function"? blog post. Color is approachable but such a common word in conversation that readers may not realize it is a "term of art". It is also widely used in some domains, such as GUIs, making it a poor choice for keyword (not that we are proposing a keyword). Also, while color is relatively grammatically flexible, it can still be awkward. For example, we sometimes talk about a specific "flavor" of a trait, and saying "the async color of the Trait" doesn't sound right. There was also the problem that, as y'all are hopefully aware, the term "colored" has been associated with some awful parts of human history, and some common phrasings could have undesirable connotations.
The FAQ doesn't discuss it (perhaps it should), but there are some other concerns about effect. Its connection to academia gives it a specific meaning that is not suitable for everything we use – unsafe
, for example, feels very much like a "flavor" to users (good litmus test: would it be usable to have an unsafe Fn
trait or unsafe
closures?), and yet it isn't well described by effects as there is no function that one calls to "be" unsafe. (There are counterarguments too, the point is mostly that it is controversial, not to litigate whether or not unsafe could be considered an effect precisely.)
mut
, is that a flavor?From the FAQ:
The
mut
keyword does not qualify as a flavor per the definition in this RFC because it cannot be applied to functions or blocks. As such, it's not clear how one would extendmut
to apply to other locations like traits or closures.However, it is true that
mut
is part of what often feels like "3 modes" in Rust:self
,&self
, and&mut self
. It is possible to imagine creating some sort of flavor such that e.g. theFn
,FnMut
, andFnOnce
traits, for example, would be flavored variants of a single "callable" trait.Another similar split is
Send
vs notSend
. We don't have a keyword for this, but especially in async code it is a very real split.
I continue to think that the RFC should be accepted. However, I also continue to think I haven't nailed the framing of the RFC. I will take one last attempt here and I would like feedback on whether this framing is superior.
The primary goal of this RFC is to commit to using async Fn
as the syntax for async closures (as proposed in RFC #3668) and to adding some form of syntax for 'async flavored types' similar to what is proposed in RFC #3628. This "async flavored type" notation, currently denoted with the placeholder syntax 🏠async<$ty>
, would be equivalent to impl Future<Output = T>
(and hence its precise meaning would be dependent on where it appears, just as impl Future
can expand in different ways).
The motivation for comitting to both async Fn
and 🏠async<$ty>
syntaxes together is to create a coherent design pattern around async
that allows the user to understand all aspects of the "async flavor" of Rust as a unit. The goal is that taking sync code and making it async can be done by adding "async" and "await" in the appropriate places without hitting places where other abstractions "leak through".
The second goal of this RFC is to make strong suggestions for the syntax of future features that are under consideration. Based on async
, the RFC defines a flavor design pattern. The flavor design pattern is a series of syntax recommendations and transformations that can be applied to any "flavor keyword" K
. A flavor keyword K
is some keyword that can be applied to a function or a block, like K fn foo()
or K { /* something */ }
, and which has the "infectious" property, meaning that code with flavor K
interacts naturally with other code of the same flavor, but only in limited ways with code of other flavors. Beyond async
, other examples of existing flavor keywords are const
and unsafe
. (There are also other "flavor-like" things in Rust that do not have keywords, like functions that operate on &T/&mut T/T
; these are not described by this RFC directly.)
Based on the flavor design pattern, the RFC recommends that we use const $Trait
as the syntax to indicate a version of $Trait
in which some or all members are made const
-flavored and that we use async $Trait
as the syntax for future "async flavors" of traits that we may decide to add (e.g., async Read
). The RFC does NOT define any such traits nor propose any kind of means to define them, mechanical or otherwise; the only criteria is that K $Trait
should have a superet of the members of $Trait
but where some have been made K
-flavored (async Fn
as defined in RFC #3668 meets this definition). The RFC includes a "future possibilities" section that describe various things we could do, such as what const $Trait
might mean, how unsafe Trait
could work (and might not), and the possibility of "flavor generics" (aka, effect generics or keyword generics). None of those features are proposed in this RFC.
NM: I want to move this forward to move async closures forward. So I want to focus on concerns that would prevent people from checking the box on this RFC.
try
as a trait flavortmandry: Our previous discussions of this pattern included try
, but I don't fully see how it fits into one of the minimum requirements of a flavor: That there can be a try Trait
flavor of any trait:
Every flavor keyword
K
should support at least the following:…
K
-traits using the syntaxK $Trait
;
K
-flavored traits should offer at least the same methods, associated types, and other trait items as the unflavored trait (there may be additional items specific to theK
-flavor). Some of the items will beK
-flavored, but not necessarily all of them.
As a rough sketch, consider try Iterator
. Would we say that the existing methods with try_
variants today, like collect
, find
, reduce
, etc. are rewritten to have the same signature as the existing try_
variants, or would those just be added on top of the existing vanilla methods?
Josh: I think try
is a family of flavors. In some cases, a function will want to support any try
flavor (e.g. "pass me a closure of any flavor and I'll be the same flavor"); in some cases a function will have one specific try
flavor because it wants to return a specific error type. (And in some cases I think people may want multiple try
flavors.) That complexity is one reason try
isn't in the baseline proposal. It should be possible to write try Trait
, but in many cases you won't want a whole try
variant of the Trait, you'll want a specific function that works with try
and not try
. (e.g. sometimes you'll want try Iterator
but sometimes you'll want a regular Iterator
that has a method that supports optional flavors like try
.)
pnkfelix: Wait, where is the min req stated that any trait must admit a variant for a given flavor? I thought it was up to specific traits to decide whether they made sense to provide flavor?
tmandry: It's not.. it's saying that individual traits should be able to define a try variant. I'm trying to work out what it means in a practical example and answer the question of whether we want that.
pnkfelix: Mind referencing the specific point in the text for "there can be a try Trait
flavor of any trait" so I/we can help better?
pnkfelix: (thanks. I guess the text there admits different interpretations.)
yosh: for try
+ Iterator
I'd expect something similar to the fallible-iterator crate. But probably using Try
or Residual
rather than hard-coding Result
. That's probably not a huge leap tho!
NM: What I imagine trait try Iterator
would look like is this:
trait try Iterator {
type Item;
fn size_hint(&self) -> SizeHint;
try fn next(&mut self) -> Option<Self::Item>;
fn map<U>(self, op: impl try FnMut(Self::Item) -> U) -> impl try Iterator<Item = U>;
// ... and so on for all combinators ...
try fn for_each(&mut self, op: impl try FnMut(Self::Item)) -> ();
}
JT: there are two ways that "try" could interact with e.g. map or for-each. You want try Iterator
to have a try fn next
. But on any Iterator
, you want the ability to pass a try
closure (or an async
closure) to map
or for_each
and have it propagate that effect.
NM: IS the heart of this whether iteration can fail or iterator can produce values that have failed? The latter is not, to my mind, a "try iteration" – it is what we do quite often.
JT: you also want the ability to pass a closure that might fail with a try (or use async
or any other flavor), whether or not you have an Iterator
that itself has a flavor.
TC: I'm still a bit unsure there needs to be a separate trait definition for a try Iterator
. Setting aside backward compatibility for a moment, this seems more like a case for refinement, e.g.:
trait Gen {
type Output;
type Item;
fn next(self: Pin<&mut Self>) -> Self::Output;
}
NM: I'm trying to hold off on drilling into the details of Iterator
but I do think there is a very comparable thing here that has to do with whether you have a "flavorful iteration" or an "iteration that produces flavorful results". e.g. I think that having a "try Iterator" means that iteration (as a whole) can fail just like "async Iterator" would mean iteration must be awaited. But having an Iterator<Item = try<T>>
or Iterator<Item = async<T>>
would be an iteration that can produce things that have failed or which must be awaited.
Nadri: Iterator
is quite special, what other examples to we have?
Various ppl: Default
, Clone
, Into
, (maybe) Drop
work well with try
, async
, const
FK: In this RFC, we are not talking about how you define a given variant, right? Just a naming convention? Can we think of it as a namespacing construct?
JT: I think Iterator is a great example for the full generality; I agree that the simple cases are good too, but Iterator is a good example to examine all the cases and see how they all can make sense. Sometimes you want try Iterator<Item = T>
, sometimes you want Iterator<Item = try<T>>
, sometimes you want Iterator<T>
but you want to call for_each
or map
with a try
closure and have it return a try
, and sometimes you may want to combine those.
Nadri: sounds similar to the case of async Read
.
NM: I found that the flavor terminology helped me to talk about this, side note – e.g., a "flavored Iterator" vs an "Iterator of flavored values".
tmandry: Building on a couple of outstanding proposals, consider the following function signature…
fn debug_items(
iter: &pin mut impl async pin Iterator<Item = impl Debug>
)
There's a lot going on here, and we haven't brought in const
or unsafe
.
Note: Maybe we want pin Iterator
to actually be called Generator
, since pin
doesn't fit the definition of a flavor in this RFC.
Josh: I think we definitely don't have clear consensus yet on pin
being something we want to treat as a flavor. But more generally…
I agree that stacking many keywords in a row results in some degree of confusion. However, I think it's less confusing on types (e.g. async Trait
) than it is on fn
: I think users will have some degree of confusion about whether they want try async fn
or async try fn
, and which order the types occur in the return type. I think it'll be less confusing and more obvious if users write return values like try<🏠E, async<T>>
or async<try<🏠E, T>>
; those have an obvious meaning and ordering.
NM: I want to focus on this question, because really all the RFC is saying is "hey we like keywords", so if we don't like having a series of keywords…that's a problem. I would rather we look at try
than pin
because I think pin
adds some orthogonal considerations. This is the kind of example that both super well motivates this and which is a bit frightening:
fn debug_items(
iter: &mut impl async try Iterator<Item = impl Debug>
)
fn debug_items(
iter: &mut impl AsyncTryIterator<Item = impl Debug>
)
TM: I'm interested also in the transition around the keyword impl
– before that, we have properties of the reference (mut
), afterwards, properties of the trait, is that something we expect users to pick up on? How do we teach that?
FK: what I expect to be part of the story is that IDEs will be able to do much better with the first version. The second version is going to have a whole slew of options. That to me is an argument that the fear of keywords may be misplaced, tooling may be able to help a lot better.
JT: I do think there'll be some amount of confusion when stacking a pile of keywords in front of fn. I don't think it's going to be confusing on traits/types, it'll be obvious what order it applies in when applied to a trait or type, but if I write async try fn
vs try async fn
, what order do I remove those from the fn and apply those to the return value? I think fn func() -> async<try<T>>
will be more obvious in that respect.
TC: By order, you mean like try<async<Fn>>
, that kind of thing?
JT: Right, do you have a Result<impl Future<Output = T>, ...>
or an impl Future<Output = Result<T, ...>>
?
NM: I don't know that the ordering will be significant here. It could be, but I don't know that it should be. The designs I favor don't include ordering. But also, I think what FK is saying is really about there being an advantage of having first class notion of flavors.
JT: (I'd be very surprised if we don't support both orderings; both orderings make sense.)
FK: Yes, and I would expect e.g. rustdoc to support it. (E.g. via presenting the flavored variants of a trait as a sublist under the trait itself in the overview panel)
NM: Good point, I definitely have had rustdoc integrating flavors in mind, and as you were saying it I realized I never really described it. I like the term "namespacing", that is a good word I should adopt.
fn debug_items(
iter: &mut (impl (async try Iterator<Item = impl Debug>))
)
fn debug_items(
iter: &mut impl [async, try] Iterator<Item = impl Debug>
)
fn debug_items(
iter: &mut impl async+try Iterator<Item = impl Debug>
)
fn debug_items(
iter: &mut impl async Iterator<Item = impl Debug>
)
fn debug_items(
iter: &mut impl Iterator[async]<Item = impl Debug>,
iter: &mut impl Iterator[async try]<Item = impl Debug>
)
TM: Given the two options of keyword vs camelcase, just have to weigh the tradeoffs. I do find the idea that you can import one name and add flavors to it later quite compelling. I find it kind of annoying that you have to go import TryFrom
everytime you want to use that instead of regular From
, for example. IDE support is kind of a point in favor but at the same time we can always teach the conventions like Async
as a prefix?
TC: I have to say that impl Iterator[async, try]
is not that bad.
JT: I think async gen
is a notably interesting example. async gen
is kind of a separate flavor onto itself that might not just be a combination of async+gen. But async try
and try async
make sense as distinct flavors and many of them make sense in different combinations. async gen
is unusual in that regard. Theoretically you might want a "gen async" that generates asyncs, but it's not obvious if async gen
is just an async
of gen
.
NM: What we're deciding on here mostly is a first-class notion of flavors rather than just a convention, and that has value. When something works 90% only, it's often suprisingly different to support it in things like IDEs. Most of the time, syntax highlighting is present, and it does affect how one parses things in one's mind. We should think too about how rustdoc
presents these things.
NM: Also I continue to want to put a pin in the question of ordering, I prefer to have it be a set, but either way we need a string of keywords.
Yosh: Swift added first-class support for displaying overloads, that might be a great starting point for us to reference.
NM: Nice. Maybe I can steal some screenshots to convey the idea.
Yosh: Vera (Misdreavus) came back with an example: Searchable (Swift Doc).
K
-block syntax with explicit type?Josh:
K
-blocks using the syntaxK { $expr }
(and potentiallyK move { $expr }
)
I think we should mention the possibility of K<ty> { $expr }
blocks, to specify the return type.
I also hope that we can find a way to avoid the stutter in fn func() -> K<ty> { K { ... } }
.
NM: I would be happy to extend the discussion of adding a syntax for flavored types, I can see it being a nice addition. Also this discussion has reinforced the value of having a syntax for flavored types, being able to distinguish a flavored iterator from an iterator over flavored types feels really useful.
JT: The thing that might not be obvious is that we should use the same syntax for "apply flavor to type" (e.g. async<T>
) and "flavor block with an explicit type" (e.g. async<T> { ... }
), rather than having the two be gratuitously different.
TC: There is ascription in the RFC. Search for async ->
.
This name is not binding in that it doesn't correspond to anything we expect to use in the language as of yet, but it is likely to appear in documentation or explanatory material, so it's worth thinking seriously about.
TC: If we have time, let's talk about this.
Perhaps I'm concerned that it may be reductive to try to group:
unsafe
const
async
…into one thing, as there are some critical differences, such as the opposite "polarity" of const
, and that by trying to reduce these so far, that's pushing us into a kind of fuzzy and jargony name.
JT: There are two distinct points here that I think we should handle separately. One, do we pick a name meaningful in the literature? Two, what non-meaningful name do we pick? I think it is correct to not use effect, as I think that brings a lot of preconceptions that we might not want to conform to. Once we've decided to use a non-meaningful name, flavor feels like as good a name as any.
TC: I think people's comments go to the first part. The main thrust is "why are you picking a meaningless name when there is already a name established in the literature"? The things that fall outside of that, maybe they shouldn't be in this category at all? Why pick a name that doesn't have meaning and then try to force things into it? I'm sympathetic that trying to pick a name that is not effect is maybe a bridge too far, maybe the constraints of the medium in picking a name that has established meaning is a useful constraint?
Nadri: I feel like part of the point here is that we are not trying to do the mechanistic derivation. The try flavor of Read
doesn't read to me "read that does the effect try
"– there are choices in doing the flavor thing. In my mind that reads as a good use of "not the technical word".
TC: In what sense don't we want that?
Nadri: We don't want that try Read
is a trait that is automatically defined from Read
by automatically adding try
to all its methods?
JT: Not sure if we don't want it, but we are not committing to it. I think it might be useful to be able to have a way to say "if you'd like have a flavor of this trait, here is the methods that will get that flavor". I don't think we should precommit to not doing this, just as I don't think we should precommit to doing it.
TC: I don't really associate effects as doing some automatic rewriting, no languages have it that I know of. The effects are more like "bounds", communicating what's true or what has to be true.
NM: Personally, (a), I do feel that unsafe is an effect, and I think the counterarguments are wrong. But also (b) either way, anything that makes you want to copy and paste a trait and add variants (which definitely includes unsafe
), feels like a thing, a pattern, and I like having a name for it. I'm also curious TC what you think of some of the other arguments, mostly the ability to use "flavor" in so many ways (e.g., flavored type) that feels really natural and useful. I admit to some bias that if you use academic terminology you make everything less accesible–not always, but the batting average is very poor.
TM: I'd like some clarity about whether we consider Send
to be a flavor. Feels relevant. Also, there was some support for this syntax, should we do a straw poll?
fn debug_items(
iter: &mut impl Iterator[async]<Item = impl Debug>,
iter: &mut impl Iterator[async, try]<Item = impl Debug>
)
Josh: Kinda like the concept of putting flavors into some kind of bracket, but the use of square brackets is breaking my brain: in type syntax, square brackets are arrays, and shouldn't be something else.
Iterator[async, try] |
Iterator[async try] |
async try Iterator |
Other | |
---|---|---|---|---|
nikomatsakis | no¹ | "maybe" | +1 | +1 |
tmandry | +0.5 | +0.75 | +1 | |
scottmcm | ||||
pnkfelix | ||||
Josh | -0* | -0* | +1 | depends on the proposal |
TC | +1 | "maybe" | +1 | |
Nadri | -0* | -0* | +1 | |
Yosh | +0 | +0 | +1 | - |
Xiang | +1 |
Josh: The -0*
here is "I might like some different kind of grouping syntax, just not square brackets because those mean 'array'". Wouldn't block, but ewww.
no¹ — I don't like the commas, in particular, I feel like it is … making multiple arguments instead of 1.
Nadri: *
here is: I want bracket syntax (and the name "effect") for "real" effects e.g. algebraic effect with effect generics etc
NM: FWIW, I think flavors can be compatible with algebraic effects, but let's discuss separately =)
Nadri: let's :)
(The meeting ended here.)
move
K
-blocks using the syntaxK { $expr }
(and potentiallyK move { $expr }
);
TC: It's a nit as far as this RFC goes, but I've come to think that we put move
in the wrong place with async
, and that we should have been consistent with closure syntax. I.e.:
move || {}
move async {}
This matters when we think of how to do ascriptions, e.g.:
move || -> u8 { 0 }
move async -> u8 { 0 }
I would like to fix this.
Josh: I feel that this is correlated with the syntax used for ascriptions. If writing async -> T
, I agree that that doesn't work well with async move
(async -> T move
and async move -> T
both seem wrong). But if writing async<T>
, I think the current ordering is fine.
In any case, we've already shipped async move
, so at best we can only add support for the other order. And even if we thought the other order was more correct, we shouldn't confuse people by having new constructs only support the new order and not the old one; e.g. newflavor move
should still work because async move
does and it'd be a confusing inconsistency otherwise.
TC: We e.g. could fix this when switching to use
, if we did that. Consider also we need to make choices about closure versions:
move async gen || -> u8 { 0 }
async move gen || -> u8 { 0 } // ?
async gen move || -> u8 { 0 } // ?
async gen || move -> u8 { 0 } // ?
(Assuming for the sake of this example we had non-()
gen
return types.)
AsyncRead
would be an anti-pattern?pnkfelix: Just to check my comprehension: Some reviewers said that they objected to async Read
on the basis that there should not be any primary async variant of the trait Read
.
pnkfelix: So, is it fair to conclude: "If we eventually conclude that there exists a sensible+primary async variant for trait Read
, then it should be denoted by async Read
; and if we eventually conclude that there does not exist any such sensible+primary variant, then it should have some name other than AsyncRead
" (and therefore in all universes, trait AsyncRead
is an anti-pattern).
yosh: I believe that is a sensible conclusion, yes. This RFC does not attempt to answer whether certain translations make sense, only what their syntax should look like if we decide to introduce them.
pnkfelix: I raise this mostly to try to say "maybe there exist other potential flavors that could help motivate the proposed pattern" – (unfortunately, I worry that all hypothetical async flavors, other than async Fn{,Mut,Once}
, may fall into the same trap in which async Read
has fallen).
yosh: Can you clarify what trap that is?
pnkfelix: The trap being that there does not exist a primary async flavor for Read; I'm worried that all (most?) traits will end up in same boat for async.
Nadri: we have Default
, Clone
, Into
etc as examples that would be straightforwardly async
. Generally I expect traits that manipulate data in a "functional" way to have a straightforward extension to async
/try
.
tmandry: From the FAQ it isn't clear to me whether we would consider Send
as a flavor… we probably want a Send Trait
where Trait
has async fn
methods.
tmandry: Let's talk about it.
Josh: It does have some of the properties of flavors: if you want to optionally be mut, you have to act as if you might be mut (so you can't have multiple borrows of the same thing that might be mut).