or
or
By clicking below, you agree to our terms of service.
New to HackMD? Sign up
Syntax | Example | Reference | |
---|---|---|---|
# Header | Header | 基本排版 | |
- Unordered List |
|
||
1. Ordered List |
|
||
- [ ] Todo List |
|
||
> Blockquote | Blockquote |
||
**Bold font** | Bold font | ||
*Italics font* | Italics font | ||
~~Strikethrough~~ | |||
19^th^ | 19th | ||
H~2~O | H2O | ||
++Inserted text++ | Inserted text | ||
==Marked text== | Marked text | ||
[link text](https:// "title") | Link | ||
 | Image | ||
`Code` | Code |
在筆記中貼入程式碼 | |
```javascript var i = 0; ``` |
|
||
:smile: | ![]() |
Emoji list | |
{%youtube youtube_id %} | Externals | ||
$L^aT_eX$ | LaTeX | ||
:::info This is a alert area. ::: |
This is a alert area. |
On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?
Please give us some advice and help us improve HackMD.
Do you want to remove this version name and description?
Syncing
xxxxxxxxxx
2024-10-23: Flavor RFC
Goal of the meeting
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
.Summary of RFC #3710
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
(whereK
is some keyword likeasync
orconst
) as a pattern that we will use going forward to define a "K-variant ofTrait
". 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 flavorK
interacts naturally with other code with flavorK
but only interacts in limited ways with code without the flavorK
. Every flavor keywordK
should support at least the following:K
-functions using the syntaxK fn $name() -> $ty
;K
-blocks using the syntaxK { $expr }
(and potentiallyK move { $expr }
);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.K
-closures using the syntaxK [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 theK
-type, the type that results from aK
-block,K
-function, orK
-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 aK
-flavored return type and body.K fn $name($args) -> $ty { $expr }
fn $name($args) -> 🏠K<$ty> { K { $expr } }
Not part of this RFC
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 theFn
traits:Read
trait, it will be referred to asasync 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 beconst Trait
; it does not specify what aconst
-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.Summary of the discussion thread
The primary topics on the discussion thread have been as follows.
Non-mechanical mechanisms
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 theK
-flavor of trait have a superset of the members of the unflavored version of the trait, not how the. From the FAQ:We then discussed a hypothetical alternative in which users could explicitly declare the "async flavor" of a trait, perhaps with a syntax like:;
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, atrait async X
declaration is not declaring a separate traitX
but rather theasync
-flavor for the traitX
. Like animpl
, this is not something one can import, but rather a connection made by the type system.The syntax
async Read
implies a "primary" read trait for asyncThe RFC only commits us to adding an
async
flavor of theFn
trait, but it does state that any future "async-flavored" traits will be referred toasync 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: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 ofRead
, 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 ofRead
's functionality but with some members madeasync
. 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
andOwnedRead
, that allow for being more specific. Note that nrc's async-flavored read also includes methods likeas_ready
not found in the syncRead
. We would have to decide whether to make distinct sync-and-async flavors ofReadyRead
andOwnedRead
or to have those traits be async-only.Read
but instead add async-only traits with different names (e.g.,BorrowedRead
andCompletionRead
). We can comment onRead
that there is no async flavor ofRead
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.
"Just add async" is a mirage, as is the correspondence between
async
andconst
The premise of generalizing
async Fn
to apply to other traits likeasync Read
is that people will be able to move code from sync-to-async by just addingasync
. 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:No response has been drafted yet because I only saw this right now!
Keyword syntax has practical disadvantages
Other concerns about
async Fn
as a syntax:Choice of name
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:
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):
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 anunsafe Fn
trait orunsafe
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.)What about
mut
, is that a flavor?From the FAQ:
An alternative summary
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 toimpl Future<Output = T>
(and hence its precise meaning would be dependent on where it appears, just asimpl 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 aroundasync
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 keywordK
is some keyword that can be applied to a function or a block, likeK fn foo()
orK { /* something */ }
, and which has the "infectious" property, meaning that code with flavorK
interacts naturally with other code of the same flavor, but only in limited ways with code of other flavors. Beyondasync
, other examples of existing flavor keywords areconst
andunsafe
. (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 madeconst
-flavored and that we useasync $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 thatK $Trait
should have a superet of the members of$Trait
but where some have been madeK
-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 whatconst $Trait
might mean, howunsafe 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.Discussion
Attendance
Meeting roles
High level
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 atry Trait
flavor of any trait:As a rough sketch, consider
try Iterator
. Would we say that the existing methods withtry_
variants today, likecollect
,find
,reduce
, etc. are rewritten to have the same signature as the existingtry_
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 anytry
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 specifictry
flavor because it wants to return a specific error type. (And in some cases I think people may want multipletry
flavors.) That complexity is one reasontry
isn't in the baseline proposal. It should be possible to writetry Trait
, but in many cases you won't want a wholetry
variant of the Trait, you'll want a specific function that works withtry
and nottry
. (e.g. sometimes you'll wanttry Iterator
but sometimes you'll want a regularIterator
that has a method that supports optional flavors liketry
.)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?- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Learn More →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 usingTry
orResidual
rather than hard-codingResult
. That's probably not a huge leap tho!NM: What I imagine
trait try Iterator
would look like is this:JT: there are two ways that "try" could interact with e.g. map or for-each. You want
try Iterator
to have atry fn next
. But on anyIterator
, you want the ability to pass atry
closure (or anasync
closure) tomap
orfor_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 anIterator
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.: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 anIterator<Item = try<T>>
orIterator<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 withtry
,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 wantIterator<Item = try<T>>
, sometimes you wantIterator<T>
but you want to callfor_each
ormap
with atry
closure and have it return atry
, 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".
Do keywords add more complexity than we realize?
tmandry: Building on a couple of outstanding proposals, consider the following function signature…
There's a lot going on here, and we haven't brought in
const
orunsafe
.Note: Maybe we want
pin Iterator
to actually be calledGenerator
, sincepin
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 onfn
: I think users will have some degree of confusion about whether they wanttry async fn
orasync 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 liketry<🏠E, async<T>>
orasync<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
thanpin
because I thinkpin
adds some orthogonal considerations. This is the kind of example that both super well motivates this and which is a bit frightening: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
vstry async fn
, what order do I remove those from the fn and apply those to the return value? I thinkfn 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 animpl 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.
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 regularFrom
, for example. IDE support is kind of a point in favor but at the same time we can always teach the conventions likeAsync
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. Butasync try
andtry 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 ifasync gen
is just anasync
ofgen
.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:
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 ->
.Choice of name
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 effecttry
"– 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 fromRead
by automatically addingtry
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?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
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 etcNM: FWIW, I think flavors can be compatible with algebraic effects, but let's discuss separately =)
Nadri: let's :)
(The meeting ended here.)
Nit about
move
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 withasync
, and that we should have been consistent with closure syntax. I.e.:This matters when we think of how to do ascriptions, e.g.:
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 withasync move
(async -> T move
andasync move -> T
both seem wrong). But if writingasync<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 becauseasync 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:(Assuming for the sake of this example we had non-
()
gen
return types.)Does everyone agree that
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 thetrait 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 byasync Read
; and if we eventually conclude that there does not exist any such sensible+primary variant, then it should have some name other thanAsyncRead
" (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 whichasync 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 straightforwardlyasync
. Generally I expect traits that manipulate data in a "functional" way to have a straightforward extension toasync
/try
.Send as a flavor
tmandry: From the FAQ it isn't clear to me whether we would consider
Send
as a flavor… we probably want aSend Trait
whereTrait
hasasync fn
methods.mut as a flavor
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).