深入浅出Rust Future - Part 2

译自Rust futures: an uneducated, short and hopefully not boring tutorial - Part 2

Intro

在这个系列的第一篇文章我们了解了如何使用Rust Future.但是只有我们彻底的了解Future并且操作得当才能发挥它真正的作用。这个系列的第二篇文章,我们将介绍如何避免Future里常见的陷阱。

Error troubles

我们将Future组织成一个很简单,只要通过Rust Future提供的and_then函数就可以了。但是在上一篇文章中我们使用了Box<Error> trait作为错误类型,绕过了编译器的检查。为什么我们没有使用更为详细的错误类型?原因很简单, 每个Future函数的错误返回都有可能不同.

原则1: 当我们将不同的Future组织成一个调用时,每个Future都应该返回相同的Error Type.

让我们一起来证明一下这一点.

我们有两个被叫做ErrorAErrorBError类型, 我们将会实现error::Error trait,尽管这并不是编译器必须让我们做的(但是这是一个好习惯[在我看来这应该算是一个最佳实践]),在我们实现error::Error trait的同时还需要实现std::fmt::Display,现在就让我们一起实现他吧!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#[derive(Debug, Default)]
pub struct ErrorA {}

impl fmt::Display for ErrorA {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "ErrorA!")
}
}

impl error::Error for ErrorA {
fn description(&self) -> &str {
"Description for ErrorA"
}

fn cause(&self) -> Option<&error::Error> {
None
}
}

// Error B
#[derive(Debug, Default)]
pub struct ErrorB {}

impl fmt::Display for ErrorB {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "ErrorB!")
}
}

impl error::Error for ErrorB {
fn description(&self) -> &str {
"Description for ErrorB"
}

fn cause(&self) -> Option<&error::Error> {
None
}
}

我尽量用简单的方式去实现Error Trait,这样可以排除别的干扰来证明我的观点. 现在让我们在Future中使用ErrorAErrorB.

1
2
3
4
5
6
7
fn fut_error_a() -> impl Future<Item = (), Error = ErrorA> {
err(ErrorA {})
}

fn fut_error_b() -> impl Future<Item = (), Error = ErrorB> {
err(ErrorB {})
}

现在让我们在main函数里调用它.

1
2
3
4
5
let retval = reactor.run(fut_error_a()).unwrap_err();
println!("fut_error_a == {:?}", retval);

let retval = reactor.run(fut_error_b()).unwrap_err();
println!("fut_error_b == {:?}", retval);

跟我们所预见的结果一致:

1
2
fut_error_a == ErrorA
fut_error_b == ErrorB

到现在为止还挺好的,让我们把ErrorAErrorB打包成一个调用链:

1
let future = fut_error_a().and_then(|_| fut_error_b());

我们先调用fut_error_a然后再调用fut_error_b,我们不用关心fut_error_a的返回值所以我们用_省略不用. 用更复杂的术语解释就是: 我们将impl Future<Item=(), Error=ErrorA>impl Future<Item=(), Error=ErrorB>打包成调用链.

现在让我们尝试编译这段代码:

1
2
3
4
5
6
7
8
9
10

Compiling tst_fut2 v0.1.0 (file:///home/MINDFLAVOR/mindflavor/src/rust/tst_future_2)
error[E0271]: type mismatch resolving `<impl futures::Future as futures::IntoFuture>::Error == errors::ErrorA`
--> src/main.rs:166:32
|
166 | let future = fut_error_a().and_then(|_| fut_error_b());
| ^^^^^^^^ expected struct `errors::ErrorB`, found struct `errors::ErrorA`
|
= note: expected type `errors::ErrorB`
found type `errors::ErrorA`

这个报错非常明显, 编译器期待我们使用ErrorB但是我们给了一个ErrorA

原则2: 当我们组织Future Chain时,第一个错误类型必须与最后一个future返回的错误类型一致.(When chaining futures, the first function error type must be the same as the chained one.)

rustc已经非常明确的告诉我们了. 这个Future chain最终返回的是ErrorB所以我们第一个函数也应该返回ErrorB. 在上述代码我们返回了ErrorA, 所以导致编译失败.

我们改如何处理这个问题?非常幸运的是, 我们可以使用Rust Future给我们提供的map_err方法. 在我们的示例中,我们想要把ErrorA转换成ErrorB,所以我们只需要在ErrorAErrorB之间调用这个函数就行了.

1
2
3
4
5
6
7
8
9
let future = fut_error_a()
.map_err(|e| {
println!("mapping {:?} into ErrorB", e);
ErrorB::default()
})
.and_then(|_| fut_error_b());

let retval = reactor.run(future).unwrap_err();
println!("error chain == {:?}", retval);

如果我们现在编译并运行示例,将会输出:

1
2
mapping ErrorA into ErrorB
error chain == ErrorB

让我们进一步推动这个例子.假设我们想连接ErrorA,然后是ErrorB,然后再连接ErrorA。 就像是:

1
2
3
let future = fut_error_a()
.and_then(|_| fut_error_b())
.and_then(|_| fut_error_a());

我们最初的解决方式只适合成对的future, 并没有考虑其他的情况。所以在上面代码中我们不得不这么做:
ErrorA => ErrorB => ErrorA.就像这样:

1
2
3
4
5
let future = fut_error_a()
.map_err(|_| ErrorB::default())
.and_then(|_| fut_error_b())
.map_err(|_| ErrorA::default())
.and_then(|_| fut_error_a());

看上去不那么优雅但是还是解决了多个Future的错误处理.

“From” to the rescue

简化上述代码的一种简单的方式就是利用std::covert::From. 当我们实现From, 这样编译器就可以自动的将一个结构软换为另一个结构.现在让我们实现From<ErrorA> for ErrorBFrom<ErrorB> for ErrorA.

1
2
3
4
5
6
7
8
9
10
11
impl From<ErrorB> for ErrorA {
fn from(e: ErrorB) -> ErrorA {
ErrorA::default()
}
}

impl From<ErrorA> for ErrorB {
fn from(e: ErrorA) -> ErrorB {
ErrorB::default()
}
}

通过上述的实现我们只需要用from_err函数来代替map_err就好了。

1
2
3
4
5
let future = fut_error_a()
.from_err()
.and_then(|_| fut_error_b())
.from_err()
.and_then(|_| fut_error_a());

现在的代码仍然与错误转换混合, 但转换代码不再是内联的,而且代码可读性也提高了。Futrue Crate非常聪明:只有在错误的情况下才会调用from_err代码, 因此在不使用from_err时, 也不会在Runtime时产生额外的开销.

Lifetimes

Rust签名功能是引用的显式生命周期注释. 但是,大多数情况下,Rust允许我们避免使用生命周期省略来指定生命周期.让我们看看它的实际效果. 我们想编写一个带字符串引用的函数,如果成功则返回相同的字符串引用:

1
2
3
fn my_fn_ref<'a>(s: &'a str) -> Result<&'a str, Box<Error>> {
Ok(s)
}

注意代码中 <'a> 的部分, 意思是我们显示的声明一个生命周期. 接着我们声明了一个引用形参s: &'a str, 这个参数必须在'a生命周期有效的情况下使用.使用Result <&'str,Box <Error >>,我们告诉Rust我们的返回值将包含一个字符串引用.只要’a有效,该字符串引用必须有效.换句话说,传递的字符串引用和返回的对象必须具有相同的生命周期.这会导致我们的语法非常冗长,以至于Rust允许我们避免在常见情况下指定生命周期。 所以我们可以这样重写函数:

1
2
3
fn my_fn_ref(s: &str) -> Result<&str, Box<Error>> {
Ok(s)
}

但是在Future中你不能这样写, 让我们来尝试用Future方式复写这个函数:

1
2
3
fn my_fut_ref_implicit(s: &str) -> impl Future<Item = &str, Error = Box<Error>> {
ok(s)
}

编译将会失败(rustc 1.23.0-nightly (2be4cc040 2017-11-01):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Compiling tst_fut2 v0.1.0 (file:///home/MINDFLAVOR/mindflavor/src/rust/tst_future_2)
error: internal compiler error: /checkout/src/librustc_typeck/check/mod.rs:633: escaping regions in predicate Obligation(predicate=Binder(ProjectionPredicate(ProjectionTy { substs: Slice([_]), item_def_id: DefId { krate: CrateNum(15), index: DefIndex(0:330) => futures[59aa]::future[0]::Future[0]::Item[0] } }, &str)),depth=0)
--> src/main.rs:39:36
|
39 | fn my_fut_ref_implicit(s: &str) -> impl Future<Item = &str, Error = Box<Error>> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

note: the compiler unexpectedly panicked. this is a bug.

note: we would appreciate a bug report: https://github.com/rust-lang/rust/blob/master/CONTRIBUTING.md#bug-reports

note: rustc 1.23.0-nightly (2be4cc040 2017-11-01) running on x86_64-unknown-linux-gnu

thread 'rustc' panicked at 'Box<Any>', /checkout/src/librustc_errors/lib.rs:450:8
note: Run with `RUST_BACKTRACE=1` for a backtrace.

当然也有解决方式,我们只要显示声明一个有效的生命周期就行了:

1
2
3
fn my_fut_ref<'a>(s: &'a str) -> impl Future<Item = &'a str, Error = Box<Error>> {
ok(s)
}

impl Future with lifetimes

Future中如果有引用传参我们必须要显示的注释生命周期. 举个例子, 我们希望使用&s的值并且返回的是一个没有引用的String.我们必须显示的注释生命周期:

1
2
3
fn my_fut_ref_chained<'a>(s: &'a str) -> impl Future<Item = String, Error = Box<Error>> {
my_fut_ref(s).and_then(|s| ok(format!("received == {}", s)))
}

上面的代码将会报错:

1
2
3
4
5
error[E0564]: only named lifetimes are allowed in `impl Trait`, but `` was found in the type `futures::AndThen<impl futures::Future, futures::FutureResult<std::string::String, std::boxed::Box<std::error::Error + 'static>>, [closure@src/main.rs:44:28: 44:64]>`
--> src/main.rs:43:42
|
43 | fn my_fut_ref_chained<'a>(s: &'a str) -> impl Future<Item = String, Error = Box<Error>> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

为了解决这个错误我们必须为impl Future追加一个'a生命周期:

1
2
3
fn my_fut_ref_chained<'a>(s: &'a str) -> impl Future<Item = String, Error = Box<Error>> + 'a {
my_fut_ref(s).and_then(|s| ok(format!("received == {}", s)))
}

现在你可以运行这段代码了:

1
2
3
4
let retval = reactor
.run(my_fut_ref_chained("str with lifetime"))
.unwrap();
println!("my_fut_ref_chained == {}", retval);

Closing remarks

在下一篇文章中,我们将介绍Reactor。 我们还将从头开始编写未来的实现结构。