Rust 可太难了:主流应用场景中的折磨

文章翻译自 Rust Is Hard, Or: The Misery of Mainstream Programming。非直译。

使用 Rust 会遇到一些很痛苦的情况,就是有时候一些喜闻乐见的简单逻辑会牵涉到超出预料的语言知识、编程技巧,而且你还要付出大量的心血去写代码,折腾半天最后还是没弄好只能摆烂。失落之余你可能会到 Reddit 找找解决办法,突然你就发现自己的代码设计竟然是在理论层面上行不通,元凶居然是语言设计 bug。

我已经用 Rust 口胡代码四年多了,是个老司机。现在我搞了 teloxide 和 dptree 库,写了些文章,还翻译了一些编程语言发行公告。我也用 Rust 写过生产环境代码,有幸在一些线上聚会发过言。但是直到现在,我发现 Rust 的所有权检查和类型系统还是和我作对,而且问题还不是出在自己犯浑上——cannot return reference to temporary value 这种错误已经折磨不到我了,我花了很长时间整了点策略来对付生命周期……

但是近期又一个情况把我干碎了。


负责处理更新事件的函数:第一次尝试

我正在开发一个巨 TM 快的聊天 bot,给人们操蛋的生活带来些愉悦。首先用长连接或者 webhooks 来获取服务器更新。对于每个更新,有一堆 handler 在等着,这些 handler 需要描述更新的对象的引用,然后返回一个返回为 () 的 future。Dispatcher 负责处理这些步骤,把每一个更新事件依次传递给 handler。

我们先试着搞搞。忽略处理器的具体执行流程,先重点关注 push_handler 函数。

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
use futures::future::BoxFuture;
use std::future::Future;

#[derive(Debug)]
struct Update;

type Handler = Box<dyn for<'a> Fn(&'a Update) -> BoxFuture<'a, ()> + Send + Sync>;

struct Dispatcher(Vec<Handler>);

impl Dispatcher {
fn push_handler<'a, H, Fut>(&mut self, handler: H)
where
H: Fn(&'a Update) -> Fut + Send + Sync + 'a,
Fut: Future<Output = ()> + Send + 'a,
{
self.0.push(Box::new(move |upd| Box::pin(handler(upd))));
}
}

fn main() {
let mut dp = Dispatcher(vec![]);

dp.push_handler(|upd| async move {
println!("{:?}", upd);
});
}

这里每个 handler 都用一个动态的 Fn trait 来表示,并限制以 HRTB 生命周期 for<'a>,因为 handler 返回的 future 的返回值应该和其输入参数的生命周期相关。有了这个东西之后,我们声明一个 Dispatcher 去管理 Vec<Handler>push_handler 函数接受一个返回 Fut 的静态泛型 H。同时为了把这静态玩意加进 Vec,我们还得把它用 Box 包一层,还要用 Box::pin 把 future 包成 BoxFuture

代码看着挺好,可惜跑不起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
error[E0312]: lifetime of reference outlives lifetime of borrowed content...
--> src/main.rs:17:58
|
17 | self.0.push(Box::new(move |upd| Box::pin(handler(upd))));
| ^^^
|
note: ...the reference is valid for the lifetime `'a` as defined here...
--> src/main.rs:12:21
|
12 | fn push_handler<'a, H, Fut>(&mut self, handler: H)
| ^^
note: ...but the borrowed content is only valid for the anonymous lifetime #1 defined here
--> src/main.rs:17:30
|
17 | self.0.push(Box::new(move |upd| Box::pin(handler(upd))));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

如果你看懂了,其实它就是在说 push_handler具体的周期 'a(通过泛型,'a 会在具体的代码里代指一个具体的周期)在代码中被转化为了 HRTB 生命周期 for<'a>,这就要求 for<'a>'a 在任何情况下都必须在 push_handler'a 内。

可惜不可能,所以寄。

不过这东西能稍微改改:咱们不用 Fut,让用户自己去处理 for<'a> 去。

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
use futures::future::BoxFuture;

#[derive(Debug)]
struct Update;

type Handler = Box<dyn for<'a> Fn(&'a Update) -> BoxFuture<'a, ()> + Send + Sync>;

struct Dispatcher(Vec<Handler>);

impl Dispatcher {
fn push_handler<H>(&mut self, handler: H)
where
H: for<'a> Fn(&'a Update) -> BoxFuture<'a, ()> + Send + Sync + 'static,
{
self.0.push(Box::new(move |upd| Box::pin(handler(upd))));
}
}

fn main() {
let mut dp = Dispatcher(vec![]);

dp.push_handler(|upd| {
Box::pin(async move {
println!("{:?}", upd);
})
});
}

终于编译通过了,但是 API 丑得离谱:用户需要自己把 handler pin 起来。可是 push_handler 本身就是为了隐藏把静态类型的 handler 转化成等价的动态类型的过程。那这种写法有个锤子意义?

怎么办呢?如果不能转化到动态类型,那就另辟蹊径,干脆不转化。

第二次尝试:异构列表(Heterogenous list)

异构列表其实就是个元组。既然不能转化成动态类型,那我们就造一个 (H1, H2, H3, ...) 这样的元组出来,每个 H 都是不同的 handler 类型。这种情况下我们需要合理设计 push_handlerexecute 来迭代这个元组。Rust 并没有提供这个功能,不过没关系,我们可以写些微妙的代码。

首先定义异构列表:

1
2
3
4
5
6
struct Dispatcher<H, Tail> {
handler: H,
tail: Tail,
}

struct DispatcherEnd;

可以看到它实际是个链表的结构。如果你现在就感觉这玩意多少有点意义不明,那你很有远见。我们先接着往下看。

在有了这样一个结构之后,我们就能构造 Dispatcher<H1, Dispatcher<H2, Dispatcher<H3, DispatcherEnd>>> 这种奇葩玩意来表示 (H1, H2, H3)。现在 push_handler 的签名就可以用归纳的写法:

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
trait PushHandler<NewH> {
type Out;
fn push_handler(self, handler: NewH) -> Self::Out;
}

impl<NewH> PushHandler<NewH> for DispatcherEnd {
type Out = Dispatcher<NewH, DispatcherEnd>;

fn push_handler(self, handler: NewH) -> Self::Out {
Dispatcher {
handler,
tail: DispatcherEnd,
}
}
}

impl<H, Tail, NewH> PushHandler<NewH> for Dispatcher<H, Tail>
where
Tail: PushHandler<NewH>,
{
type Out = Dispatcher<H, <Tail as PushHandler<NewH>>::Out>;

fn push_handler(self, handler: NewH) -> Self::Out {
Dispatcher {
handler: self.handler,
tail: self.tail.push_handler(handler),
}
}
}

如果你曾经折腾过 C艹 的模板(或者干脆接触过编译时计算的各种奇技淫巧),那你可能对这种写法很熟悉。如果没太看明白的话你可以考虑一下递归,唯一的不同是从变量的递归换到了类型上递归而已。

  • 边界条件impl<NewH> PushHandler<NewH> for DispatcherEnd。这里创建了仅有一个 handler 的 dispatcher。
  • 每一步impl<H, Tail, NewH> PushHandler<NewH> for Dispatcher<H, Tail>。这里只会递归 self.tail

execute 也长得差不多:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
trait Execute<'a> {
#[must_use]
fn execute(&'a self, upd: &'a Update) -> BoxFuture<'a, ()>;
}

impl<'a> Execute<'a> for DispatcherEnd {
fn execute(&'a self, _upd: &'a Update) -> BoxFuture<'a, ()> {
Box::pin(async {})
}
}

impl<'a, H, Fut, Tail> Execute<'a> for Dispatcher<H, Tail>
where
H: Fn(&'a Update) -> Fut + Send + Sync + 'a,
Fut: Future<Output = ()> + Send + 'a,
Tail: Execute<'a> + Send + Sync + 'a,
{
fn execute(&'a self, upd: &'a Update) -> BoxFuture<'a, ()> {
Box::pin(async move {
(self.handler)(upd).await;
self.tail.execute(upd).await;
})
}
}

一切看起来岁月静好,直到接下来一步。现在,我们需要实现一个统一的 execute,它必须能处理不同类型的更新事件——毕竟我们写的是个库。不过看一看上面的 Execute 的签名,它是依赖于某个具体的生命周期 'a 的。

似乎有一丝不妙的气息。

为了能够应对所有的生命周期,execute 长成这个样子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
async fn execute<Dp>(dp: Dp, upd: Update)
where
Dp: for<'a> Execute<'a>,
{
dp.execute(&upd).await;
}

#[tokio::main]
async fn main() {
let dp = DispatcherEnd;

let dp = dp.push_handler(|upd| async move {
println!("{:?}", upd);
});
execute(dp, Update).await;
}

然而一编译……

1
2
3
4
5
6
7
8
error: implementation of `Execute` is not general enough
--> src/main.rs:83:5
|
83 | execute(dp, Update).await;
| ^^^^^^^ implementation of `Execute` is not general enough
|
= note: `Dispatcher<[closure@src/main.rs:80:30: 82:6], DispatcherEnd>` must implement `Execute<'0>`, for any lifetime `'0`...
= note: ...but it actually implements `Execute<'1>`, for some specific lifetime `'1`

两眼一黑.jpg

现在还感觉所有权检查很简单吗?上面这个代码永远不可能编译通过,不管继续折腾什么写法都不可能。原因也很简单:交给 dp.push_handler 的闭包接受一个具体的生命周期 '1,但是 execute 又要求 Dp 对任意生命周期 '0 都有实现。核心问题和第一次遇到的一模一样。不过如果我们把闭包换成一个普通的函数,这段代码居然就跑起来了:

1
2
3
4
5
6
7
8
9
10
11
#[tokio::main]
async fn main() {
let dp = DispatcherEnd;

async fn dbg_update(upd: &Update) {
println!("{:?}", upd);
}

let dp = dp.push_handler(dbg_update);
execute(dp, Update).await;
}

看起来似乎有点离谱,事实上所有权检查的这种行为离谱至极。函数和闭包不仅签名不同,其处理生命周期的策略也大不一样。闭包签名的生命周期是具体的,而像是函数 dbg_update 中的周期则是动态的,能够接受任意的 'a。一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
let dbg_update = |upd| {
println!("{:?}", upd);
};

{
let upd = Update;
dbg_update(&upd);
}

{
let upd = Update;
dbg_update(&upd);
}

跑起来就会报错:

1
2
3
4
5
6
7
8
9
10
error[E0597]: `upd` does not live long enough
--> src/main.rs:11:20
|
11 | dbg_update(&upd);
| ^^^^ borrowed value does not live long enough
12 | }
| - `upd` dropped here while still borrowed
...
16 | dbg_update(&upd);
| ---------- borrow later used here

dbg_update 签名的生命周期在第一次调用时就固定到了具体的生命周期上,所以第二次调用时就寄了。

作为对比,如果把 dbg_update 写成函数就不会有任何问题。

1
2
3
fn dbg_update_fn(upd: &Update) {
println!("{:?}", upd);
}

如果把函数的签名展开:

1
2
3
4
5
6
7
8
9
10
11
12
// let () = dbg_update_fn;

error[E0308]: mismatched types
--> src/main.rs:9:9
|
9 | let () = dbg_update_fn;
| ^^ ------------- this expression has type `for<'r> fn(&'r Update) {dbg_update_fn}`
| |
| expected fn item, found `()`
|
= note: expected fn item `for<'r> fn(&'r Update) {dbg_update_fn}`
found unit type `()`

看到那个 for<'r> 了吗?

总之,异构列表又麻烦又原始,太 trick 还完全不能在闭包上工作,根本不是我们想要的解决方案。同时我也建议不要在 Rust 的类型机制里玩花样,否则我只能祝你好运,千万千万别遇上像这种 dispatcher 的类型错误。想象一下你在维护一个生产环境代码,要尽快修复一个 bug,改了点代码结果遇上了这东西:

1
2
3
4
5
6
7
8
9
10
error[E0308]: mismatched types
--> src/main.rs:123:9
|
123 | let () = dp;
| ^^ -- this expression has type `Dispatcher<for<'_> fn(&Update) -> impl futures::Future<Output = ()> {dbg_update0}, Dispatcher<for<'_> fn(&Update) -> impl futures::Future<Output = ()> {dbg_update1}, Dispatcher<for<'_> fn(&Update) -> impl futures::Future<Output = ()> {dbg_update2}, Dispatcher<for<'_> fn(&Update) -> impl futures::Future<Output = ()> {dbg_update3}, Dispatcher<for<'_> fn(&Update) -> impl futures::Future<Output = ()> {dbg_update4}, Dispatcher<for<'_> fn(&Update) -> impl futures::Future<Output = ()> {dbg_update5}, Dispatcher<for<'_> fn(&Update) -> impl futures::Future<Output = ()> {dbg_update6}, Dispatcher<for<'_> fn(&Update) -> impl futures::Future<Output = ()> {dbg_update7}, Dispatcher<for<'_> fn(&Update) -> impl futures::Future<Output = ()> {dbg_update8}, Dispatcher<for<'_> fn(&Update) -> impl futures::Future<Output = ()> {dbg_update9}, DispatcherEnd>>>>>>>>>>`
| |
| expected struct `Dispatcher`, found `()`
|
= note: expected struct `Dispatcher<for<'_> fn(&Update) -> impl futures::Future<Output = ()> {dbg_update0}, Dispatcher<for<'_> fn(&Update) -> impl futures::Future<Output = ()> {dbg_update1}, Dispatcher<for<'_> fn(&Update) -> impl futures::Future<Output = ()> {dbg_update2}, Dispatcher<for<'_> fn(&Update) -> impl futures::Future<Output = ()> {dbg_update3}, Dispatcher<for<'_> fn(&Update) -> impl futures::Future<Output = ()> {dbg_update4}, Dispatcher<for<'_> fn(&Update) -> impl futures::Future<Output = ()> {dbg_update5}, Dispatcher<for<'_> fn(&Update) -> impl futures::Future<Output = ()> {dbg_update6}, Dispatcher<for<'_> fn(&Update) -> impl futures::Future<Output = ()> {dbg_update7}, Dispatcher<for<'_> fn(&Update) -> impl futures::Future<Output = ()> {dbg_update8}, Dispatcher<for<'_> fn(&Update) -> impl futures::Future<Output = ()> {dbg_update9}, DispatcherEnd>>>>>>>>>>`
found unit type `()`

实际情况下报错能比这个长 20 倍,我就问你怎么办。

第三次尝试:Arc

遇事不决上 Arc。

当我还是个菜鸡时,我还以为借用就和智能指针差不多。现在只要没有性能问题,我到处都用 Rc/Arc。不管你信不信吧,前面这些操蛋的问题全都是因为 type Handler 里参数的生命周期 'a

让这破玩意爬,咱们用 Arc<Update>

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
use futures::future::BoxFuture;
use std::future::Future;
use std::sync::Arc;

#[derive(Debug)]
struct Update;

type Handler = Box<dyn Fn(Arc<Update>) -> BoxFuture<'static, ()> + Send + Sync>;

struct Dispatcher(Vec<Handler>);

impl Dispatcher {
fn push_handler<H, Fut>(&mut self, handler: H)
where
H: Fn(Arc<Update>) -> Fut + Send + Sync + 'static,
Fut: Future<Output = ()> + Send + 'static,
{
self.0.push(Box::new(move |upd| Box::pin(handler(upd))));
}
}

fn main() {
let mut dp = Dispatcher(vec![]);

dp.push_handler(|upd| async move {
println!("{:?}", upd);
});
}

Ah♂. That’s good♂. 一切都变得明朗起来。我们甚至不用在闭包里手动声明 Arc,一切都自己解决了。

Rust 的问题

“无惧并发”,这是正确但充满误导的话。你确实不用面对数据竞争问题了,但是却有了别的麻烦。

我解释下。在前几小节里,我其实还没告诉你 Rust 的所有限制和不足,以及这些问题到底造成了什么影响。首先你应该注意到大量用 Box 包装的 futures:先前的这些 BoxFutureBox::newBox::pin 都无法用泛型替代。如果你多少了解 Rust,就会知道 Vec 只能存储编译期大小确定的类型,所以我们需要在 type Handler 里用 BoxFuture。然而用 BoxFuture 来写函数签名多少有点不可读,相比 async 来讲。

Niko Matsakis 写的「为什么 trait 中的异步函数难以实现」解释了导致这一问题的原因。简而言之,在编写本文章时,我们还不能在 trait 里定义 async fn,我们只能用一些类型擦除的替代方法,比如 async-trait 库或者像上文一样使用 BoxFuture。实际上 async-trait 做的事情和咱们差不多,只不过我想避免使用这东西,它的过程宏把编译期错误搞得乱七八糟。BoxFuture 也有缺点——例如你得给所有 async fn 函数加上 #[must_use],否则编译器不会提示在 execute 上使用 await。事实上把静态实例 box 住的操作很常见,futures 库就导出了许多常用 trait 的动态类型,比如 BoxStreamLocalBoxFutureLocalBoxStream(后两个没有 Send)。

第二,upd 的显式类型声明毁掉了一切:

1
2
3
4
5
6
7
8
9
10
11
12
13
use tokio; // 1.18.2

#[derive(Debug)]
struct Update;

#[tokio::main]
async fn main() {
let closure = |upd: &Update| async move {
println!("{:?}", upd);
};

closure(&Update).await;
}

编译器报错:

1
2
3
4
5
6
7
8
9
10
11
error: lifetime may not live long enough
--> src/main.rs:8:34
|
8 | let closure = |upd: &Update| async move {
| _________________________-______-_^
| | | |
| | | return type of closure `impl Future<Output = ()>` contains a lifetime `'2`
| | let's call the lifetime of this reference `'1`
9 | | println!("{:?}", upd);
10 | | };
| |_____^ returning this value requires that `'1` must outlive `'2`

(你可以试试删掉: &Update,代码就能编译了。)

如果你没搞明白这个报错的意思,那不是你的问题——看看 issue #70791。看一眼这个 issue 的标签是不是有个 C-Bug,这是个编译器 Bug。在写这篇文章时,rustc 有 3107 个未解决的 C-bug issue 和 114 个 C-bug+A-lifetimes issue。还记得上文提到的 async fn 和闭包的问题了吗?那也是个编译器 bug。还有很多 2020 年前的语言 bug,比如 issue #41078issue #42940

你可以看到这个简单的 handler 注册操作到底是如何一级级无缝提升到 rustc 的错误身上,虽然我们在极力避免语言上的问题。设计 Rust 接口就像是在穿越雷区:你必须平衡你的选择,既要设计一个理想的接口,又要考虑到底能使用哪些特性。并不是所有的语言都是这个鬼样子。对于一些稳健的语言,我们能预感到接口可能会用到哪些语言特性;但是在 Rust 里设计 API 总是会被大量不同的语言限制影响。你希望所有权检查能确保引用合法、希望类型系统能应对各种实体,结果却是 BoxPinArc 满天飞,还要和类型系统斗智斗勇。

在结束前,我就放一个 golang 的等价实现在这:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

type Update struct{}
type Handler func(*Update)

type Dispatcher struct {
handlers []Handler
}

func (dp *Dispatcher) pushHandler(handler Handler) {
dp.handlers = append(dp.handlers, handler)
}

func main() {
dp := Dispatcher{handlers: nil}
dp.pushHandler(func(upd *Update) {
fmt.Println(upd)
})
}

为毛 Rust 这么难?

理解这坨翔为什么会这么臭对我们是有意义的。

Rust 是一个系统级语言。为了达到这一点,编程语言不能隐藏底层的内存管理过程。所以 Rust 强迫我们去处理大量细节,这些东西在高阶语言里往往会被隐藏起来:比如指针、引用、内存分配、不同的字符串类型、不同的 Fn、pin 等等。

Rust 是一个静态语言。静态类型系统的语言一般需要同时实现静态和动态级别的特性。把一个静态类型转换为对应的动态类型称为上转型;逆过程称为下转型。在 push_handler 里我们就用到了上转型。

另外,Rust 还想做到内存安全,又想把一些事务变得直观。这些牛逼的特性组合在一起,强调了计算机语言设计的人类界限。现在你应该了解为什么 Rust 里有着各种缺陷,事实上这东西竟然能用起来本身就是个奇迹。计算机语言就像是个紧密交织在一起的组件系统:你想引入一个新的语言特性,就得确保新特性不会和系统其他部分冲突。我觉得应该为那些全职开发这种语言的人们提供免费的医疗保险和各种优待。

如何改善这些问题?

现在想象一下,Rust 的所有问题都消失了。而且整个 rustc 和 std 都经过了形式化验证。如果能有一个完整的语言规范且有多个一级实现,或者对硬件平台的支持能达到 GCC 的水平,抑或有稳定的 ABI(尽管不清楚如何处理泛型),或者类似的东西,那也是相当不错的。这可能就是一种理想中的系统级编程语言了。

或者想象一下 Rust 的所有问题都消失而且是一个彻头彻尾的高阶编程语言。那现在主流的编程语言都会被揍出屎来。Rust 有足够的常规特性,它支持多态,还有一个非常爽的包管理器。我不想列举其他主流语言的问题:JavaScript 那有毒的语法、Java 那个企业级怪物、C 的空指针、C艹的未定义行为、C# 的冗余语法……现代语言就像场滑稽的表演。就算这些语言都有着各种各样的缺点,人们还是能写出软件来。Rust 还远不是主流编程语言,我觉得 Rust 也永远不会像 Java 和 Python 一样受欢迎。这是个社会问题。因为这些天生的复杂性,Rust 的高级工程师一定会比 Java、Python 少,而且这些人需要更高的薪水。作为一个雇主想找到好的雇员也更难。

最后,想象一下,Rust的问题消失了,是高阶语言,并且有功能集。这大概会接近理想情况了,能为大众提供一种高级的、通用的编程语言。有趣的是,设计这样一种语言可能会比原来的 Rust 更简单,因为我们可以把所有低级别的细节隐藏在一个语言运行时的外壳之下。

静待更好的未来

所以,如果我「想通了」,为什么我不开发一个更好的 Rust 呢?考虑到我的语言脱颖而出的机会是极小的,我不想把未来二十年的时间花在这上面。我认为目前使用最多的语言在某种程度上是很随机的——我们总是可以解释为什么一种语言会流行,但我们一般无法解释为什么更好的替代品会消失在历史长河。是来自大公司的支持?还是瞄准了 IT 趋势?原因是相当社会化的。残酷的现实:在生活中,有时运气比你所有的技能和自我奉献发挥着更重要的作用。

如果你还想创造一个属于未来的编程语言,那我祝你好运,也祝你身心健康。你充满勇气,以及没有意义的浪漫。