- Start Date: 2024-10-15
- RFC PR: rust-lang/rfcs#3710
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 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;- Closures implement the
K Fn
traits.
- Closures implement the
Some flavors rewrite code so that it executes differently (e.g., async
). These are called rewrite flavors. Each such flavor should have the following:
- A syntax
🏠K<$ty>
defining theK
-type, the type that results from aK
-block,K
-function, orK
-closure whose body has type$ty
.- The
🏠K<$ty>
is a placeholder. We expect a future RFC to define the actual syntax. (The 🏠 emoji is meant to symbolize a "bikeshed".)c
- The
- A "do" operation that, when executed in a
K
-block, consumes a🏠K<$ty>
and produces a$ty
value. - The property that a
K
-function can be transformed to a regular function with aK
-flavored return type and body.- i.e., the following are roughly equivalent (the precise translation can vary so as to e.g. preserve drop order):
K fn $name($args) -> $ty { $expr }
fn $name($args) -> 🏠K<$ty> { K { $expr } }
- i.e., the following are roughly equivalent (the precise translation can vary so as to e.g. preserve drop order):
Existing flavor-like keywords in the language do not have all of these parts. The RFC therefore includes a limited set of binding recommendations that brings them closer to conformance:
- Commit to
K $Trait
as the syntax for applying flavors to traits, with theasync Fn
,async FnMut
, andasync FnOnce
traits being the only current usable example. - Commit to adding a TBD syntax
🏠async<$ty>
that will meet the equivalences described in 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):
- Any form of "effect" or "flavor" generics:
- Flavors in this RFC are a pattern for Rust designers to keep in mind as we explore possible language features, not a first-class language feature; this RFC also does not close the door on making them a first-class feature in the future.
- Whether or how
async
can be used with traits beyond theFn
traits:- For example, the RFC specifies that if we add an async flavor of the
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.
- For example, the RFC specifies that if we add an async flavor of the
- How
const Trait
ought to work (under active exploration):- The RFC only specifies that the syntax for naming a
const
-flavored trait should beconst Trait
; it does not specify what aconst
-flavored trait would mean or when that syntax can be used.
- The RFC only specifies that the syntax for naming a
- What specific syntax we should use for
🏠K<$ty>
:- We are committed to adding this syntax at least for
async
, but the precise syntax still needs to be pinned down. RFC #3628 contains one possibility.
- We are committed to adding this syntax at least for
The Rust Project has a flagship goal of bringing the async Rust experience closer to parity with sync Rust, and one of the most important deliverables for that goal is async closures. The only unresolved question in the async closure RFC is the syntax for async closure bounds, which could be spelled like F: async Fn()
or F: AsyncFn()
(or many other ways). For reasons covered more in depth in that RFC, neither of these is an obvious choice given the Rust we have today.
The lang team discussed the syntax question in a design meeting and concluded that we prefer offering the async Fn
syntax as a means of requesting the "async flavor of a trait", but we would want it to be part of a consistent syntactic design pattern for selecting variants of traits.
In other words, if users wrote async Fn
to get an async flavor of a closure but AsyncRead
to get the async flavor of the Read
trait, that would be confusing. In contrast, if the pattern to get the async flavor of a trait is always to write the async
keyword (e.g., async Fn
and async Read
), that is appealing. This is true even if the async Read
trait is defined in a very separate way from its sync counterpart.
We also observed that there is a need for having "variants of traits" in other similar contexts, most notably const
, where there is active experimentation for a const-trait design. If we further extend the pattern to cover those cases, so that one writes (e.g.) const Fn
or const Default
to the "const flavors of the Fn
or Default
traits" respectively, then these two instances of the same pattern reinforce one another, helping to create a coherent whole. Any future instances we add would only strenghten this effect.
Looking more closely at async
and const
, we see that there is a latent concept within Rust that we refer to as flavors. Each flavor has an associated keyword K
that can be applied to functions and blocks. Code with flavor K
interoperates fully with other code with the same flavor; working across flavors is generally more limited:
async
functions return animpl Future
which can only beawait
'd from anotherasync
block.unsafe
functions can only be called fromunsafe
blocks or otherunsafe
functions.const
functions can only call otherconst
functions.
The goal for all Rust flavors is to be consistent with unflavored Rust overall. Generally speaking, adding a flavor to some code should just require adding the flavor keyword in the right places, along with other complementary keywords (e.g., await
for async
). One goal of this RFC is to outline all the places that flavors must be supported to enable this "just add flavor" feel. We do this by exploring the example of async
(and then exploring const
, which requires a subset of those places).
The premise of this RFC is that flavor keywords like const
, async
, and unsafe
"feel similar" to users and that we should make them work as consistently as possible (but no more than that). Because they serve different purposes, we don't wish to force them into a single shape if that shape is an ill-fit. Nonetheless, we wish to ensure that these flavors, and any future "flavor-like" features, feel as consistent and predictable as possible, so that users can develop "muscle memory" that helps them to predict syntax they haven't used or seen yet. The RFC describes the syntax to use for a flavor keyword applied to various constructs as well as some of the "approximate equivalences" that flavors should generally hold.
Code with a given flavor K
is meant to interoperate fully with other code with the same flavor, but in practice that is not the case. This is precisely the gaps we are aiming to close by considering features like async closures or const traits. The role of this RFC is to identify the "flavor pattern" and carry it to its logical conclusion.
Existing flavors do not yet support all the aspects described herein. This RFC recommends changes that close a limited number of these gaps; the Future Possibilities discusses other changes we could make to more fully support the flavor pattern for every flavor.
One of the recommendations of this RFC is committing to add a K
-flavored type syntax in the future, similar to what is proposed in RFC #3628. The motivation for this is that it is important when teaching flavors to be able to teach the flavor specifically, without having to reference additional language features like impl Trait
. This addresses feedback we have received from Rust trainers which is that one of the difficulties in teaching Rust is that users can't learn Rust in "layers", they have to learn all parts of Rust at once before any of it makes sense.
Just because flavors share many elements doesn't make them equivalent. We also describe how the flavor pattern applies to const
and show why some parts of the pattern don't make sense in that context. We think that const
should use the pattern where it makes sense, but avoid it (or use a distinct pattern) where it does not fit.
Our guide-level explanation proceeds in three phases. We begin by defining the flavor pattern for rewrite flavors, using async
as our running example. A rewrite flavor K
is one that, like async
, transforms the result type for K
-flavored functions and blocks into a distinct type, reflecting the impact of the flavor.
Next, we define a subset of the flavor pattern appropriate for filter flavors. A filter flavor K
is one that, like const
or unsafe
, tracks the presence of absence of kinds of operations. Since filter flavors do not change how code executes, they also do not change the result type of a K
-block, so only some parts of the flavor pattern are relevant.
Finally, we close with a section that explains the async
flavor, making use of the various parts of the pattern described in the first section.
We begin by defining the criteria where we, as language designers, should consider using the flavor pattern for a keyword K
. Given the flavor patterns informality, there is no hard and fast rule for this, but the primary criteria to consider are as follows:
- The keyword
K
is used to "flavor" code, in the form of functions and blocks. K
-flavored code works best when used with otherK
-flavored code; working with "unflavored" code is limited.
Apart from these minimal criteria, flavors can encompass a broad range of keywords with diverse effects. In Rust today, there are three keywords that meet this definition: async
, const
, and unsafe
. This section focuses on async
; the next section covers const
and unsafe
.
The syntax to use when applying flavors to functions, blocks, traits, and closures is as follows:
K fn $name($args) -> $ty { $expr }
for functionsK { $statements; $expr }
(and in some casesK move { $statements; $expr }
) for blocksK $Trait
for traits (generalizing the precedent set by RFC #3668)K |$args| $expr
for closures (generalizing the precedent set by RFC #3668)
Function and block syntax lines up with existing, stable async
syntax:
async fn foo() { }
let future = async move { /* ... */ };
Trait and closure syntax follows the precedent described in RFC #3668:
async Fn
as the async flavor of theFn
traitasync || /* ... */
as the syntax for an async closure.
Across all examples we see a consistent pattern that async
appears as a prefix, consistent with its role as a kind of adjective (which in English grammar appears first).
- Resolving the unresolved question from RFC #3668 in the affirmative (yes, we will use the
async Fn
syntax). - Committing to use
async $Trait
as the syntax for any futureasync
-flavored traits we may add. - Committing to use
K $Trait
as the syntax for any other form ofK
-flavored traits we may add (e.g.,const
-flavored).
This RFC should NOT be understood as adding any form of K
-flavored traits. For now, the only example of a K
-flavored trait are the async Fn*
traits introduced by RFC #3668. This RFC does not permit users to name an async
flavor of any other trait. It does however specify that if we support async
flavors for other trait, they should use the syntax async $TraitName
. For example, if and when we add support for an async-flavored Drop
trait, it should be named async Drop
. Furthermore, if and when an RFC emerges from the ongoing experimentation with const traits, that RFC should use the syntax const $Trait
.
When you tag a block or function with async
, it changes its result type to impl Future<Output = $ty>
, where $ty
is the type that would normally have been produced. This characteristic is shared with some other prospective flavors (e.g., try
and gen
), but not all: it corresponds to cases where the code executes differently in some way from ordinary code.
Currently there is no special syntax for this type translation. That is, the only way to reference the "type produced by an async block" is to write an explicit impl Future
. One example where this comes up frequently is when users wish to write an async fn
that will execute some code eagerly and defer the rest to execute when the future is awaited. The way to do this today is to write a regular function that returns impl Future
:
fn do_something() -> impl Future<Output = ()> {
// ... actions take here execute immediately ...
async move {
// ... actions here execute when future is awaited ...
}
}
Having users write impl Future<Output = T>
explicitly in cases like this has downsides. To start, it is verbose. But also, as a new user of Rust, it requires understanding several unnecessary concepts (the trait system, the Future
trait, impl Trait
syntax) in order to write async code like this.
To address these issues, RFC #3628 proposes adding a new syntax for "the type that results from an async
block", but that RFC has not been accepted. Accepting the RFC would require answering two questions:
- Does it make sense to add some syntax beyond
impl Future<Output = T>
for the result of an async block? - If so, precisely what syntax should we use?
- The RFC proposes
async<T>
, but other syntaxes have been proposed, such asasync -> T
,impl async -> T
, andimpl Future -> T
.
- The RFC proposes
In discussion the lang team concluded that having an explicit syntax for "the result of an async block" that was tied to the async
keyword was a good idea. However, similarly to the question about trait syntax, the syntax is most desirable if it is part of a larger pattern. In this case, it is part of two larger patterns: one is using the async
keyword as a prefix to transform all things related to async (functions, blocks, traits, closures, and now types). The second is that other future flavor keywords may employ a similar syntax (e.g., try
or gen
).
This RFC commits us to adding some form of this syntax for async
but defers the bikeshed for RFC #3668 or some future RFC. We refer to this future syntax 🏠async<$ty>
. Usage of this syntax should be equivalent to typing impl Future<Output = $ty>
. Note that 🏠async<$ty>
is not a Rust type alias but a syntactic substitution, and therefore the impl
keyword here has a different role depending on where it appears (in function arguments, it corresponds to a new generic argument to the fn; in return position, it corresonds to an abstract return type, etc).
This RFC also recommends that future flavors which "transform" the result of a block or function introduce a corresponding syntax.
Where 🏠K<$ty>
exists it should meet the following invariants:
- A
K
-functionK fn foo() -> $ty { $expr }
returns a value of type🏠K<$ty>
when called.- The body
$expr
produces/returns a value of the declared type$ty
.
- The body
- A
K
-blockK { $expr }
produces a result of type🏠K<$ty>
, where:$ty
is the type produced by the body$expr
(orbreak
statements that target this block).
- A
K
-closureK |$args| -> $ty { $expr }
returns a value of type🏠K<$ty>
when called.- The body
$expr
produces/returns a value of the declared type$ty
. - The
K
-closure implements the traitK Fn<Output = T>
.
- The body
- There is some operation, called the "do" operation, that takes a
🏠K<$ty>
value and returns a$ty
value to the surrounding code.
- The definition of
🏠async<$ty>
isimpl Future<Output = $ty>
. - The "do" operation for
async
isawait
.
Looking at async functions and the proposed 🏠async<$ty>
notation, we observe the following relationship. An async
function like count_input
...
async fn count_input(urls: &[Url]) -> usize {
// ... iterate over urls and load data ...
}
...can be transformed into an "ordinary" function that (a) returns 🏠async<$ty>
and (b) uses an async move
block (caveat: the true transformation is slightly more subtle in order to preserve the drop order for method arguments):
fn count_input(urls: &[Url]) -> 🏠async<usize> {
async move {
// ... iterate over urls and load data ...
}
}
This works because async blocks rewrite their content to produce a different value.
This RFC recommends that future flavors that define 🏠K<$ty>
meet this invariant:
- A
K fn foo() -> T
should be equivalent to a (unflavored)fn
that returns🏠async<T>
and uses some form ofK
-flavored block.
The previous section defined the flavor pattern in full with reference to async
. The async
flavor has the effect of rewriting the blocks it is applied to so that the produce a different kind of value. For this reason, we call it a rewrite flavor. The other extant flavors in Rust (unsafe
, const
) do not have this property. Instead, they are used to track the presence or absence of certain kind of operations. We call these filter flavors; as we will explain, the full flavor pattern doesn't make sense for filter flavors, so we define the subset that does still apply. This section focuses on const
specifically; unsafe
is discussed in the Future Possibilities section.
Both async and const can be applied to blocks and functions, but there are important differences between them. Async changes how a function body is compiled and the type of value that is produced; the result of async functions (and async blocks) are only usable from other async contexts.
In contrast, const
limits the operations a function can perform to those that the compiler can execute at runtime. It does not change how a function is compiled at runtime nor does it change the type of value that is produced.
The relationship between async
functions and async
blocks is also different in kind than the relationship between const
functions and const
blocks. An async
function is sugar for a function with an async
block in its body (and a different return type). In contrast, a const
function is something that can be run at compilation or runtime, and a const
block is code that only runs at compilation time.
And yet, despite all these differences, both const
and async
intuitively feel very similar -- they both feel like flavors to users, as argued in the motivation. How to understand this?
The natural orientation for flavors is additive: having a K
-flavored block gives access to additional capabilities (like awaiting values). const
, in contrast, is a subtractive flavor. A const
function can do fewer things than an ordinary function. To see the difference, imagine an alternate form of Rust, Runtime-Rust.
"Runtime-Rust" works exactly like Rust, but the defaults are flipped: code blocks, by default, execute at compilation time. To execute at runtime, they must be tagged with runtime { ... }
, which in turn gives them access to capabilities that are only permitted at runtime, like FFI. This runtime
flavor matches async
much more closely: just as async
blocks can only fully be used from inside another async
block, runtime
blocks can only be used from inside runtime
blocks. What this thought experiment shows is that const
is actually an inverted default, like ?Sized
.
Continuing with the Runtime-Rust hypothetical, most parts of the flavor pattern apply equally well to runtime
and async
:
K
-flavored functions, blocks, and closures are prefixed with the keywordK
.K
-flavored traits are prefixed with the keywordK
.- A
K
-flavored traitTrait
include (at least) the members ofTrait
, with some of them flavored byK
.
- A
K
-flavored functions and closures implementK
-flavored variants of theFn
trait.
There is one part however that doesn't really fit:
K
-flavored types can be written with the syntax🏠K<T>
:K
-flavored functions, closures, and blocks return/produce🏠K<T>
values, whereT
is their original type.
Introducing a 🏠runtime<T>
syntax to describe a "runtime-flavored type" doesn't make sense, because runtime
blocks don't change the type of value that is produced. So 🏠runtime<T> = T
always. We don't need syntax for this and having it would be confusing. Furthermore, the equivalence between a K
-function and a function whose body returns a K
-block doesn't hold:
runtime fn $name() -> T { $expr }
is NOT equivalent tofn $name() -> T { runtime { $expr } }
This equivalence doesn't work because the runtime
flavor cannot be encapsulated. You can't have a compilation time function that uses a runtime
block internally but hides it from its callers. In contrast, you can have a synchronous Rust function that creates an async
block internally and executes it by polling in place or some other means.
Based on our discussion of runtime
, we can identify the parts of the full flavor pattern that apply to filter flavors:
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. 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;- Closures implement the
K Fn
traits.
- Closures implement the
The other parts of the pattern (e.g., the 🏠K<ty>
syntax or defining a "do" operation) do not apply because filter flavors don't change the type of the value produced by the block. Further, the transformation from K fn $name
to a function with a K
-block doesn't work, because the point of a K fn $name()
is to signal to callers that $name
can only be called inside of a K
-block.
The "flavor pattern" is meant to guide Rust designers as we extend the language, not to be something directly taught to end users. To illustrate how it might feel, this section covers an example of teaching
async
leveraging the syntax and concepts of the flavor pattern (but not teaching the pattern explicitly). To help illustrate where the overall vision for Rust, we assume that🏠K<$T>
isK -> $T
(a variant of RFC #3628 currently preferred by the author), thatasync -> T
and its equivalentimpl Trait<Output = T>
are supported in local variable declarations (RFC #2071), and that there is some form ofasync Iterator
trait available in std.
Rust's async
functions are a built-in feature designed for building concurrent applications. Async functions are just like regular functions, but prefix with the async
keyword:
async fn load_data(input_urls: &[Url]) -> Vec<Data> {
let mut data = Vec::new();
for url in input_urls {
// Load the data from `url` and merge it into `data`
data.push(load_url(url).await);
}
data
}
async fn load_url(url: &Url) -> Data {
// ... do something ...
}
When you call an async
function, the function does not execute immediately. Instead, it returns a suspended function execution, called a future. This future can later be awaited to cause it to execute. We see this in load_data
, which invokes a helper async function load_url
and then awaits the result before pushing it into data
.
If we were break that call to load_url
out into separate statements, it would look like this:
for url in input_urls {
// Calling `load_url` yields a future, the type of which is denoted as `async -> Data`:
let url_future: async -> Data = load_url(url);
// Awaiting the future causes it to execute synchronously.
// Once it completes, we have a `Data`.
let url_data: Data = url_future.await;
// Load the data from `url` and merge it into `data`
data.push(url_data);
}
Some things to note:
- The value returned by
load_url
is not of typeData
, it's of typeasync -> Data
. This notation indicates thaturl_future
is in a fact a future that, when awaited, will yield aData
value. - The notation
url_future.await
is used to await a future. This causes the current task to block and execute the function.
In our original example, we immediately awaited the result of the function call (load_url(url).await
). This is equivalent to calling a synchronous function.
Where futures become powerful is when you combine them to create concurrent patterns. For example, suppose the caller has a list of URLs urls
and would like to split it in half and process the data from the two halves concurrently:
// Split list of URLs into two pieces:
let mid_point = urls.len() / 2;
let (urls_1, urls_2) = urls.split(mid_point).unwrap();
// Create two futures by loading these halves.
// Nothing happens at this point, we just create
// two suspended computations.
let future_1: async -> Vec<Data> = load_data(urls_1);
let future_2: async -> Vec<Data> = load_data(urls_1);
// Join those two futures together into a new future.
let future: async -> (Vec<Data>, Vec<Data>) = join!(future_1, future_2);
// Await the joined future. This will process both
// halves concurrently and yield up a tuple with
// the two vectors.
let (data_1, data_2): (Vec<Data>, Vec<Data>) = future.await;
In addition to async functions, Rust supports async blocks. These are a lighterweight alternative to defining an async function and are useful when you want to suspend execution of a small piece of code that references various things from its environment, similar to a closure. For example, we might like to make a future that invokes load_data
, awaits the result, and then does some light pre-processing:
let processed_data: async -> ProcessedData = async {
load_data(urls).await.process()
};
Creating an async block yields an async -> T
future representing the suspended computation, just like the futures that result from calling an async function.
Or, to continue with our join!
example, we might load and process the data from two sets of URLs concurrently:
let (data_1, data_2) = join!(
async { load_data(urls_1).await.process() },
async { load_data(urls_2).await.process() },
).await;
Here, the futures supplied to the join!
macro are two async blocks, instead of just calls to load_data
.
Async blocks resemble closures in some ways. Just as a Rust closure by default takes references to variables from the surrounding stack frame, async blocks yield futures that store references to the variables they need whenever possible. You can however use the async move
syntax to force the future to take ownership of any data that is uses.
We previous defined load_data
using a for
loop:
async fn load_data(input_urls: &[Url]) -> Vec<Data> {
let mut data = vec![];
for url in input_urls {
data.push(load_url(url).await);
}
data
}
We saw in Chapter XX that we rewrite those for-loops to use iterators with a map
/collect
call. If you try that in load_data
, however, it won't compile, because you can't use await
in a synchronous closure:
async fn load_data(input_urls: &[Url]) -> Vec<Data> {
input_urls
.iter()
.map(|url| load_url(url).await)
// ^^^^^
// Error: await from a synchronous closure
.collect()
}
To use await
from inside the map
closure, the closure needs to be tagged as async
. This in turn requires you to use an async iterator, which you can get by invoking async_iter
. Once you make these changes, we can rewrite load_data
to use an async iterator as follows:
async fn load_data(input_urls: &[Url]) -> Vec<Data> {
input_urls
.async_iter()
.map(async |url| load_url(url).await)
.collect()
.await // <-- collect yields a future, must await
}
Meta note: We are assuming here that some kind of
async Iterator
trait is eventually added that supports functionality similar tofutures::Stream
. This functionality is not specified in this RFC.
The async fn
syntax is actually a shorthand for a more general form of declaration that makes use of async -> T
types and async
blocks. The more general declaration is formed by moving the async
keyword from before the fn
to the return type and then transforming the body to use an async move
block:
fn load_data(urls: &[Url]) -> async -> Data {
async move {
// ... same as before ...
}
}
This more general declaration gives you more control over what happens when load_data
is called. Among other things, it can let you take some actions right away, while deferring others to run when the resulting future is awaited. To do this, simply add some statements before the async move
block:
fn load_data(urls: &[Url]) -> async -> Data {
// ... this code executes immediately ...
async move {
// ... this code executes when the future is awaited ...
}
}
Many languages have some variant of async-await notation and so async functions in Rust likely look familiar to you. That's good! But be aware that async-await works differently in Rust than most other languages.
The most obvious difference is that where most languages put await
as a prefix, Rust makes it into a suffix. So instead of writing await foo
you write foo.await
. This is more convenient when writing chained operations, like data.load().await.load_more().await
, and especially when dealing with futures that yield Result
, as one can use ?
like load_data().await?
.
The other, more subtle, difference has to do with Rust's execution model. In most languages, calling an async function implicitly starts up a background task that will continue executing. Rust, like Kotlin, takes a different approach: calling an async function returns a suspended computation, which on its own is inert. That future can then by combined with other futures to form aggregates, like the join!
operation that we saw in the previous example. Eventually, the future or the aggregate that it is embedded into must be awaited -- and, when it is, that will block your current task until it completes.
If you'd like the async functon to execute in the background, then you need to use a spawn
function to create a new task (analogous to spawning a new thread in synchronous code). Rust itself does not provide a spawn function, but one is typically provided by your async runtime. The most common choice here is tokio
, which offers tokio::spawn
:
let data_future = tokio::spawn(async move { load_data(urls).await });
// ... `data` is not being loaded in the background, in parallel ...
We have thus far avoided saying precisely what a future is, apart from a "suspended computation". The more precise answer is that a future is a value that implements the Future
trait. The notation async<T>
that we have been using is in fact a shorthand for impl Future<Output = T>
, meaning "a value of some type that implements Future<Output = T>
". We introduced the impl Future
notation in Chapter XX, and async -> T
works the same way:
- When used in function argument position like
fn method(data: async -> Data)
,async -> T
is equivalent to adding a new anonymous type parameterA
whereA: Future<Output = T>
. - When used in return position like
fn method() -> async -> Data
or in a type alias liketype DataFuture = async -> Data
,async<T>
desugars to an opaque type whose precise value is inferred from the function body or uses of the type alias, respectively. - When used in local variable position like
let data: async -> Data = ...
,async -> Data
desugars to an assertion that the type ofdata
implementsFuture<Output = Data>
. - In other positions,
async -> T
(likeimpl Future<Output = T>
) is an error.
This section collects the precise statements of this RFC.
There is no hard and fast rule for this, but the primary criteria to consider are as follows:
- The keyword
K
should be applicable to functions and blocks of code.const fn foo()
,const { .. }
async fn foo()
,async { .. }
,async move { .. }
K
-flavored code should generally only be usable with otherK
-flavored code.const
functions can only call otherconst
functionsasync
functions return futures that can only be awaited from async blocks
Flavors fall into two, non-exlusive, categories:
- Rewrite flavors changes how a
K
-flavored block executes, resulting in a distinct type from the block vs an unflavored block. - Filter flavors either expand or restrict the set operations that can be performed in a
K
-flavored block.
Every flavor keyword K
supports:
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. 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;- Closures implement the
K Fn
traits.
- Closures implement the
Rewrite flavors further support:
K
-types using a TBD syntax, denoted🏠K<$ty>
for now.- A
K
-type🏠K<$ty>
is the type resulting from aK
-block whose expression has type$ty
.
- A
- A
K
-functionK fn $name($args) -> $ty { $expr }
is "roughly" equivalent to a regular function whose return type and body areK
-flavored:fn $name($args) -> 🏠K<$ty> { K { $expr } }
- The translation will vary slightly depending on the specifics of the flavor. The goal is that
K
-functions should behave as close as possible to an unflavored function. For example, drop order should be preserved.
- A
K
-closure returns🏠K<$ty>
, where$ty
would have been the return type had the closure been unflavored. - A "do" operation that, when executed in a
K
-block, consumes a🏠K<$ty>
and produces a$ty
value.
Some flavors may only include a subset of these features. This subset should be documented and explained.
Answer: nothing. It's just an observation of fact! K
is a keyword that can be applied to blocks and functions and which affects how code can interoperate.
Longer answer: it suggests that K
should support the syntaxes and operations described in this RFC. Each flavor is different, so it is possible that a given piece of syntax is not relevant, but you ought to be able to explain why. Certainly past experience with flavors suggests that over time you will want to be able to use them in all the places identified in the flavor pattern to allow people to write code naturally.g
Will there be a mechanical connection between flavors of a trait (like async Read
) and the original (Read
)?
That is not yet clear. The only flavored trait currently RFC'd is the async
flavor of the Fn
trait. It is not clear whether we will ever add an async
flavor for the Read
trait or what language mechanism we would use to do so. It could be automatically derived from Read
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 ordinary FnOnce
trait, it offers a call_once
method (but flavored async
) and an Output
associated type. As an implementation detail, it has additional members, like the CallOnceFuture
; 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).
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 extend mut
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. the Fn
, FnMut
, and FnOnce
traits, for example, would be flavored variants of a single "callable" trait.
Another similar split is Send
vs not Send
. We don't have a keyword for this, but especially in async code it is a very real split.
During the RFC period we evaluated several possible names. The term "flavor" was ultimately chosen because it did very well along three criteria:
- 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.
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.
Most parts of the flavor pattern already exist in Rust today or at least in accepted RFCs. The 🏠K<$T>
syntax for flavored types stands out as the exception. It was included in the RFC because it forms an important part of the overall story (witness how prominent it is in the guide section). Some members of the lang team felt that, without 🏠K<$T>
, they didn't feel good about the flavor pattern overall.
We considered a number of alternatives to K Trait
for denoting K-flavored traits. Perhaps the most obvious was a camelcased identifier like AsyncFn
. While this is a perfectly viable option, it has downsides as well:
- First, the story of how to transition something from sync to async gets more complicated. It's not a story of "just add
async
keywords in the right places". - Second, this convention does not offer an obvious way to support const traits like
const Default
, unless we are going to produceConstDefault
variants as well somehow. And if there are more variants in the future, e.g.,AsyncSendSomeTrait
orAsyncTrySendSomeTrait
, it becomes very unwieldy. - Third, although we are not committing to any form of "flavor generics" in this RFC, we would also prefer not to close the door entirely; using an entirely distinct identifier like
AsyncFn
would make it very difficult to imagine how one function definition could be made generic over a flavor.
Ultimately, the killer argument was that this felt like everybody's preferred "second choice option", but nobody's favorite. It's not a bold design.
The concern is that typing the name Fn
when, in fact, the trait is called AsyncFn
seemed like to confuse users. Morever, it is not building a muscle memory that can be applied to other traits. (On the other hand, the function traits are already quite built-in and have significant "language syntax" associated with them, like custom Fn()
notation in bounds and closure expressions.)
One advantage of today's sugar for trait bounds (T: Fn()
, T: FnMut()
, etc) is that it more closely resembles function declarations. Adding the async keyword continues that, in that one now puts async
in front of the fn
or closure and then likewise in front of the bound (T: async Fn()
). Iterating on that vein, we considered going further with bound notation, so that one would write T: fn()
instead of T: Fn()
and then T: async fn()
instead of T: async Fn()
. The following objections were raised:
- There is no obvious way to encode
T: FnMut()
orFnOnce()
. Should the notation beT: fn mut()
andT: fn once()
? Then we are losing the parallel to function declarations and introducing some ad-hoc keywords. - Changing the notation for
T: Fn()
bounds is a massive change affects virtually all existing Rust code. To make a change like that, we have to be very confident the change will be a win, and many were not (particularly given the previous bullet).
We deliberately chose non-standard terms to avoid associations. For example, the "filter" flavor might also be called an "effect-carrying flavor", but that invites debate as to the precise definition of an effect and whether unsafe
qualifies.
The answer to this is complicated and left to be resolved by future RFCs that actually add the 🏠K<$T>
notation. Recall that we specified that K
-flavors can be migrated from a K
-function to its return type and body:
K fn foo() -> T { ... }
// becomes
fn foo() -> 🏠K<T> { K { ... } }
The question then is whether it is possible to transform a a K
-closure in a similar way:
K |args| -> T { ... }
// could maybe become
|args| -> 🏠K<T> { K { ... } }
Supporting this notation is tricky however because closure return types are often elided. Given that K
-closure types implement K Fn
rather than the typical Fn
trait, we need to be able to determine if a closure has flavor K
to decide what traits it implements. But there is no way to distinguish whether an expression like || K { .. }
is meant to be a K
-closure that implements K Fn<Output = T>
or an unflavored closure that implements Fn<Output = 🏠K<T>>
value. As described in RFC #3668, these two traits are not equivalent for async
and likely not for other future rewrite flavors.
We've left the behavior here to be specified in a future RFC, but we note that it would be simple to declare it as an error for now. Note that -> impl Future
is also not accepted in this position.
The term "flavor" is taken from the "What flavor is your function?" blog post. As that post indicates, many languages have added flavors of one form or another. This RFC is focused on the syntactic patterns around flavors, specifically using the flavor as a prefix (e.g., async fn
, 🏠async<T>
, async Trait
). This pattern of prefixing functions with flavors is very common and is very natural in English as the flavor plays the role of an adjective, describing the kind of function one has.
Although the RFC is focused on syntax, it does also suggest some equivalences that flavors must maintain, such as how a K fn foo() -> T { expr }
can be transformed to fn foo() -> 🏠K<T> { K move { expr } }
for any rewrite flavor K
. These equivalences hint at an underlying semantics for flavors. There are two related precedents in academia:
- Haskell's monads are comparable in some ways to rewrite flavors, and the monadic laws suggest similar equivalences. A "monad" is like a flavor made up of two operations, one that "wraps" a value
v
to yield aWrapped<V>
(analogous toK-type!(v)
) and one that does anand_then
operation, often called "fold", taking aWrapped<V>
and aV => Wrapped<W>
closure and yielding aWrapped<W>
value. The effect is kind of like a "programmable semicolon", allowing Haskell programs to introduce operations in in between each statement, and to potentially carry along "user data" with the wrappedV
value. Haskell includes generic syntax ("do"-notation) designed to work with monads, allowing one to define generic functions that can be used with any monadWrapped
.- Monads are famously challenging to learn ("The sheer number of different monad tutorials on the internet is a good indication of the difficulty many people have understanding the concept", says the Haskell wiki). Flavors run the risk of being similarly complicated. However, this RFC is not introducing flavors as a concept that users would be taught, but rather as a syntactic pattern that they will pick up on as they learn about different existing parts of Rust (async, unsafe, etc).
- Ergonomic challenges with monads often arise when composing or converting between monads. These same problems arise with flavors (e.g., how to call an async function from a sync function), but those problems already exist and are not made worse by this RFC.)
- Effects in languages like Koka can be compared to flavors. Effects have evolved over the years from a way of signalling side effects (from which the name derived) to an expressive construct for injecting new operations into functions. An effect in Koka is a function that is threaded down through context, the usage of which must be declared. Because Koka is based on continuation passing style, effect functions are able to abort computation (modeling exceptions) or pause and resume it (modeling generators).
This question is purposefully left unresolved by this RFC and is meant to be addressed in follow-up RFCs, such as RFC #3628.
This question is purposefully left unresolved by this RFC. Let's use async std::io::Read
as an example. The RFC is not saying that async Read
should be mechanically derived from Read
(see also this FAQ). What the RFC does say is that we intend to support flavors and that future work will need to be done to elaborate those implications. There are some minimum requirements:
- You should think of
async $Trait
as the async flavor of$Trait
, which means that you import the "unflavored trait name" (e.g.,use std::io::Read
) and then apply theasync
keyword to it. - The methods/items available in the trait should should be a superset of the unflavored version, some of which may be "flavored" (e.g.,
async Read
has the same methods asRead
, but some of them areasync
; it may offer additional methods to account for extra functionality).
This RFC suggests a number of future changes to "fill out" the support for flavors:
- Async should add a
🏠async<T>
syntax (see RFC #3628). - We will eventually need a mechanism for defining async-flavored traits like
async Iterator
orasync Default
. For the short term this is not urgent as the traits we are focused on (such asFn
andDrop
) are special. - Const-flavored traits should use a notation like (e.g.)
const Default
. - Unsafe-flavored traits like
unsafe Default
, which would mean aDefault
trait with an unsafedefault
method, are a possibility, but they could be easily confused for an unsafe trait.
In authoring this RFC, we also explored what future flavors might look like. Three potential future flavors are unsafe
, try
(for fallible functions that yield a Result
, most commonly at least) and gen
(for generators). try
and gen
would be rewrite flavors.
Unlike async
and const
, these three keywords are not themselves flavors but more like families of flavors. unsafe
for example indicates that the function has safety predicates that must be proven before it can be called and hence the "true flavor" conceptually includes those predicates (though we don't write them explicitly in our notation). try
and gen
both have other types associated with them beyond the main output type.
unsafe
is a filter flavor. The unsafe
flavor allows operations that are not proven safe using the Rust type system and which therefore require user validation. In contrast to some filter flavors, unsafe
doesn't actually change the set of operations that a given piece of code can perform at runtime -- a safe block can invoke a safe functon which includes an unsafe block (and most do), so safe blocks can perform the same actions as unsafe blocks. Furthermore the unsafe
flavor, despite being 1 keyword, is really a "set" of flavors, in that each unsafe fn
has its own safety requirements that are distinct from one another.
Treating unsafe
as a filter flavor would mean adding the following pieces of syntax:
- an
unsafe $TraitName
syntax, where some operations in$TraitName
unsafe; - unsafe closures
|$args| $expr
that implementunsafe Fn
.
This would close an existing "wart" in the language: a safe fn foo()
implements the Fn()
trait, but an unsafe fn foo()
does not (because the "call" method in Fn
is safe). Similarly, the type fn()
of safe function pointers implements Fn
, but the type unsafe fn()
of unsafe function pointers does not.
There is a complication to considering unsafe
as a flavor because we already have a notion of an unsafe
trait, with a distinct meaning. An unsafe trait is one where implementors prove a safety predicate relied on by the caller, as opposed to an unsafe
-flavored trait, which is a trait where the caller proves a safety predicate relied upon by the impl.
It's also worth noting the unsafe_op_in_unsafe_fn
lint, which encourages the use of unsafe
blocks even within an unsafe
function. This is likely to be different from other effect-carrying flavors that could be added (e.g., perhaps one that tracks whether allocation or panics can occur in a function). This difference stems in part from the fact that unsafe
is not tracking an "effect" that occurs at runtime but rather an aid to static reasoning.
The try
keyword was introduced in RFC #243, along with the ?
operator. It has been unstable since RFC #243 was merged in 2016 and has seen significant evolution.
As described in RFC #243, a try { $expr }
block executes $expr
and "captures" any ?
operations that occur within. On successful completion, the result of the try block is not the result of $expr
but rather an "ok-wrapped" variant. The term "ok wrapping" refers to the common case where the try
block produces a Result
; the idea is that try { 22 }
would be equivalent to Ok(22)
. During the course of try
's long history, the design temporarily changed not to Ok
-wrap. However, the original design was ultimately restored.
Looking at try
as a flavor, it is clear that ok-wrapping is the correct and consistent behavior. try
, like async
, is a fully general flavor that transforms its result type, and the flavor pattern specifies that the type of an expression in a block should be "ok-wrapped" to produce the block's final output type. Another point of controversy about try
has been how to support it syntactically at the function level. This RFC suggests that the right answer is try fn foo() -> T
, where T
represents the "ok" type. The final result of calling foo()
would therefore be 🏠try<T>
, the ok-wrapped flavor of T
.
While treating try
as a flavor is appealing, it does have one important difference from async
: try
and ?
are intended to be usable with many types, including Result
, Option
, and others. In terms of the flavor pattern, it is therefore unclear what the syntax 🏠try<T>
should expand to.
One possibility is permitting the user to explicitly declare (and important) try
flavors themselves in the form of a type alias. For example, the std::io::Result<T>
pattern might instead be:
// Older alias, deprecated:
type Result<T> = Result<T, std::io::Error>;
// in std::io, we define `try` as an alias to transformed type,
// given the Ok type:
type try<T> = Result<T, std::io::Error>;
Users could then import try
from std::io
and use it:
use std::io::try;
try fn load_configuration_string() -> String {
std::fs::read_to_string("config.txt")?
}
This would be equivalent to the following unflavored function returning 🏠try<T>
:
use std::io::try;
fn load_configuration_string() -> 🏠try<String> {
try { std::fs::read_to_string("config.txt")? }
}
This is only one possibility. There are several other ideas for how try could be integrated:
- Define
type try = Result<!, std::io::Error>
as an alias for the residual and usetry -> T
to meanimpl Try<Output = T, Residual = X>
(this assumes🏠try<T>
is defined astry -> T
). - Extend
try fn
with a clause liketry fn foo() -> T throws X
and have it expand toimpl Try<Output = T, Residual = X>
or something like that.
The gen
keyword has been proposed for introducing generator blocks, which are a syntactic pattern to make it easier to write iterators (and perhaps to fill other use cases, there is a range of design space still to be covered). One notation proposed for gen
is gen<T>
, which appears at first glance to resemble the proposed async<T>
type notation from RFC #3628. However, as proposed, gen<T>
is actually quite different, as the T
here represents the type of value that is yielded by the iterator. Therefore the generator may produce any number of T
instances, whereas async<T>
produces exactly one T
instance. This means that they are used very differently by users, and it is unclear whether what parts of the flavor pattern should apply to gen
in this case.
That said, there is some precedent for treating "generators" as flavor-like things. In Haskell, the List
monad performs implicit flat_map
operations, meaning that a given piece of code may execute many times. In Rust terms, this would correspond to nested for
loops. Consider the following Rust-like pseudocode:
gen fn even() yields u32 {
// In Haskell-like pseudocode, this might be something like
//
// do
// i <- all()
// if i % 2 == 0 { return i }
//
// Here, the fact that there is a for-loop is "hidden" in the
// monad itself, and not apparent in the `do`.
//
// In contrast, under the proposed gen designs, the `for` loop
// in Rust code would be explicit, not something that happens
// via an implicit mechanism:
for i in all() {
if i % 2 == 0 {
yield i;
}
}
}
gen fn all() yields u32 {
for i in 0.. {
yield i;
}
}
Treating gen
as a flat_map
operation does not map to the "do" operation defined in the flavor pattern. The "do" operation is meant to take a transformed value of type 🏠gen<T>
and produce a single value of type T
; but flat_map
produces any number of T
values. This is not necessarily a problem, not all flavors have to provide all parts of the pattern, but it would suggest that gen
is a distinct category of flavor from a rewrite flavor like async
or try
.
There is however another "do" operation we might use for generators which does fit the rewrite pattern, but which may not be as intuitive or useful to end users. In addition to the yield type that we have been discussing, generators can be extended with the idea of a final value type; we can use the syntax 🏠gen<F, Yielding = Y>
, where Y
is the yielding type and F
is the final type that is returned. (An existing impl Iterator<Item = I>
can be seen as having a yield type of I
and a final type of ()
.) In this case, the "do" operation would be yield_all
, which would yield up all Y
items and then return the final F
value.
Here is an example using the hypothesized "yield all" construct, showing how you could work with gen<Yielding = Y>
as a flavor. In this case, the yield_all
'unwraps' the generator effect, propagating the yielded values, and returning the final value. Calling outer
would then yield up [0
, 1
, 2
, 6
, 7
, 8
] and return 42. Note how .yield_all
appears at the same places that .await
would appear if these were async functions:
// Example of `gen<Yielding = Y>` as a flavor.
//
// Clearly this syntax is suboptimal, it is only
// meant to illustrate the concept.
gen<Yielding = u32> fn outer() -> u32 {
// Yields 0, 1, 2 and returns 6 = (0 + 1 + 2) * 2
let mid = starting_at(0).yield_all;
// Yields 6, 7, 8 and returns 42 = (6 + 7 + 8) * 2
starting_at(mid).yield_all
}
gen<Yielding = u32> fn starting_at(x: u32) -> u32 {
let mut sum = 0;
for i in x .. x+3 {
sum = sum + i;
yield i;
}
sum * 2
}
Obviously this is a syntax that only a mother could love (it's a techical term). An alternative syntax might move the Yielding =
part gen
to a keyword or separate part of the declaration:
// In place of this...
gen<Yielding = u32> fn outer() -> u32 {}
// ...maybe this?
gen fn outer() -> u32 yielding u32 {}
// ...or this?
gen fn outer() -> u32, Yielding = u32 {}
Clearly more exploration is needed here. The best option may be to say that gen
does not follow the rewrite flavor pattern in its full particulars.