Add chapter: Workarounds to Know and Love

This commit is contained in:
Taylor Cramer 2019-08-20 17:20:55 -07:00 committed by Taylor Cramer
parent 2f17ce0ce3
commit b3467d9f9f
10 changed files with 307 additions and 0 deletions

View File

@ -0,0 +1,10 @@
[package]
name = "07_05_recursion"
version = "0.1.0"
authors = ["Taylor Cramer <cramertj@google.com>"]
edition = "2018"
[lib]
[dev-dependencies]
futures-preview = { version = "=0.3.0-alpha.17", features = ["async-await", "nightly"] }

View File

@ -0,0 +1,14 @@
#![cfg(test)]
#![feature(async_await)]
#![allow(dead_code)]
// ANCHOR: example
use futures::future::{BoxFuture, FutureExt};
fn recursive() -> BoxFuture<'static, ()> {
async move {
recursive().await;
recursive().await;
}.boxed()
}
// ANCHOR_END: example

View File

@ -11,4 +11,5 @@ members = [
"05_02_iteration_and_concurrency",
"06_02_join",
"06_03_select",
"07_05_recursion",
]

View File

@ -0,0 +1,6 @@
# Workarounds to Know and Love
Rust's `async` support is still fairly new, and there are a handful of
highly-requested features still under active development, as well
as some subpar diagnostics. This chapter will discuss some common pain
points and explain how to work around them.

View File

@ -0,0 +1,72 @@
# Return Type Errors
In a typical Rust function, returning a value of the wrong type will result
in an error that looks something like this:
```
error[E0308]: mismatched types
--> src/main.rs:2:12
|
1 | fn foo() {
| - expected `()` because of default return type
2 | return "foo"
| ^^^^^ expected (), found reference
|
= note: expected type `()`
found type `&'static str`
```
However, the current `async fn` support doesn't know to "trust" the return
type written in the function signature, causing mismatched or even
reversed-sounding errors. For example, the function
`async fn foo() { "foo" }` results in this error:
```
error[E0271]: type mismatch resolving `<impl std::future::Future as std::future::Future>::Output == ()`
--> src/lib.rs:1:16
|
1 | async fn foo() {
| ^ expected &str, found ()
|
= note: expected type `&str`
found type `()`
= note: the return type of a function must have a statically known size
```
The error says that it *expected* `&str` and found `()`,
which is actually the exact opposite of what you'd want. This is because the
compiler is incorrectly trusting the function body to return the correct type.
The workaround for this issue is to recognize that errors pointing to the
function signature with the message "expected `SomeType`, found `OtherType`"
usually indicate that one or more return sites are incorrect.
A fix to this issue is being tracked in [this bug](https://github.com/rust-lang/rust/issues/54326).
## `Box<dyn Trait>`
Similarly, because the return type from the function signature is not
propagated down correctly, values returned from `async fn` aren't correctly
coerced to their expected type.
In practice, this means that returning `Box<dyn Trait>` objects from an
`async fn` requires manually `as`-casting from `Box<MyType>` to
`Box<dyn Trait>`.
This code will result in an error:
```
async fn x() -> Box<dyn std::fmt::Display> {
Box::new("foo")
}
```
This issue can be worked around by manually casting using `as`:
```
async fn x() -> Box<dyn std::fmt::Display> {
Box::new("foo") as Box<dyn std::fmt::Display>
}
```
A fix to this issue is being tracked in [this bug](https://github.com/rust-lang/rust/issues/60424).

View File

@ -0,0 +1,42 @@
# `?` in `async` Blocks
Just as in `async fn`, it's common to use `?` inside `async` blocks.
However, the return type of `async` blocks isn't explicitly stated.
This can cause the compiler to fail to infer the error type of the
`async` block.
For example, this code:
```rust
let fut = async {
foo().await?;
bar().await?;
Ok(())
};
```
will trigger this error:
```
error[E0282]: type annotations needed
--> src/main.rs:5:9
|
4 | let fut = async {
| --- consider giving `fut` a type
5 | foo().await?;
| ^^^^^^^^^^^^ cannot infer type
```
Unfortunately, there's currently no way to "give `fut` a type", nor a way
to explicitly specify the return type of an `async` block.
To work around this, use the "turbofish" operator to supply the success and
error types for the `async` block:
```rust
let fut = async {
foo().await?;
bar().await?;
Ok::<(), MyError>(()) // <- note the explicit type annotation here
};
```

View File

@ -0,0 +1,89 @@
# `Send` Approximation
Some `async fn` state machines are safe to be sent across threads, while
others are not. Whether or not an `async fn` `Future` is `Send` is determined
by whether a non-`Send` type is held across an `.await` point. The compiler
does its best to approximate when values may be held across an `.await`
point, but this analysis is too conservative in a number of places today.
For example, consider a simple non-`Send` type, perhaps a type
which contains an `Rc`:
```rust
use std::rc::Rc;
#[derive(Default)]
struct NotSend(Rc<()>);
```
Variables of type `NotSend` can breifly appear as temporaries in `async fn`s
even when the resulting `Future` type returned by the `async fn` must be `Send`:
```rust
async fn bar() {}
async fn foo() {
NotSend::default();
bar().await;
}
fn require_send(_: impl Send) {}
fn main() {
require_send(foo());
}
```
However, if we change `foo` to store `NotSend` in a variable, this example no
longer compiles:
```rust
async fn foo() {
let x = NotSend::default();
bar().await;
}
```
```
error[E0277]: `std::rc::Rc<()>` cannot be sent between threads safely
--> src/main.rs:15:5
|
15 | require_send(foo());
| ^^^^^^^^^^^^ `std::rc::Rc<()>` cannot be sent between threads safely
|
= help: within `impl std::future::Future`, the trait `std::marker::Send` is not implemented for `std::rc::Rc<()>`
= note: required because it appears within the type `NotSend`
= note: required because it appears within the type `{NotSend, impl std::future::Future, ()}`
= note: required because it appears within the type `[static generator@src/main.rs:7:16: 10:2 {NotSend, impl std::future::Future, ()}]`
= note: required because it appears within the type `std::future::GenFuture<[static generator@src/main.rs:7:16: 10:2 {NotSend, impl std::future::Future, ()}]>`
= note: required because it appears within the type `impl std::future::Future`
= note: required because it appears within the type `impl std::future::Future`
note: required by `require_send`
--> src/main.rs:12:1
|
12 | fn require_send(_: impl Send) {}
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: aborting due to previous error
For more information about this error, try `rustc --explain E0277`.
```
This error is correct. If we store `x` into a variable, it won't be dropped
until after the `.await`, at which point the `async fn` may be running on
a different thread. Since `Rc` is not `Send`, allowing it to travel across
threads would be unsound. One simple solution to this would be to `drop`
the `Rc` before the `.await`, but unfortunately that does not work today.
In order to successfuly work around this issue, you may have to introduce
a block scope encapsulating any non-`Send` variables. This makes it easier
for the compiler to tell that these variables do not live across an
`.await` point.
```rust
async fn foo() {
{
let x = NotSend::default();
}
bar().await;
}
```

View File

@ -0,0 +1,53 @@
# Recursion
Internally, `async fn` creates a state machine type containing each
sub-`Future` being `.await`ed. This makes recursive `async fn`s a little
tricky, since the resulting state machine type has to contain itself:
```rust
// This function:
async fn foo() {
step_one().await;
step_two().await;
}
// generates a type like this:
enum Foo {
First(StepOne),
Second(StepTwo),
}
// So this function:
async fn recursive() {
recursive().await;
recursive().await;
}
// generates a type like this:
enum Recursive {
First(Recursive),
Second(Recursive),
}
```
This won't work-- we've created an infinitely-sized type!
The compiler will complain:
```
error[E0733]: recursion in an `async fn` requires boxing
--> src/lib.rs:1:22
|
1 | async fn recursive() {
| ^ an `async fn` cannot invoke itself directly
|
= note: a recursive `async fn` must be rewritten to return a boxed future.
```
In order to allow this, we have to introduce an indirection using `Box`.
Unfortunately, compiler limitations mean that just wrapping the calls to
`recursive()` in `Box::pin` isn't enough. To make this work, we have
to make `recursive` into a non-`async` function which returns a `.boxed()`
`async` block:
```rust
{{#include ../../examples/07_05_recursion/src/lib.rs:example}}
```

View File

@ -0,0 +1,14 @@
# `async` in Traits
Currently, `async fn` cannot be used in traits. The reasons for this are
somewhat complex, but there are plans to remove this restriction in the
future.
In the meantime, however, this can be worked around using the
[`async_trait` crate from crates.io](https://github.com/dtolnay/async-trait).
Note that using these trait methods will result in a heap allocation
per-function-call. This is not a significant cost for the vast majority
of applications, but should be considered when deciding whether to use
this functionality in the public API of a low-level function that is expected
to be called millions of times a second.

View File

@ -20,6 +20,12 @@
- [TODO: Spawning](404.md)
- [TODO: Cancellation and Timeouts](404.md)
- [TODO: `FuturesUnordered`](404.md)
- [Workarounds to Know and Love](07_workarounds/01_chapter.md)
- [Return Type Errors](07_workarounds/02_return_type.md)
- [`?` in `async` Blocks](07_workarounds/03_err_in_async_blocks.md)
- [`Send` Approximation](07_workarounds/04_send_approximation.md)
- [Recursion](07_workarounds/05_recursion.md)
- [`async` in Traits](07_workarounds/06_async_traits.md)
- [TODO: I/O](404.md)
- [TODO: `AsyncRead` and `AsyncWrite`](404.md)
- [TODO: Asynchronous Design Patterns: Solutions and Suggestions](404.md)