是 Rust 太難了,還是主流編程本來就這麼折磨人?

本文作者在文章的前部分用了大量筆墨詳細描述了自己嘗試 Rust 受挫的經歷,後半部分分析了 Rust 的問題及發展。自發布以來,這篇文章在 r/rust 上得到了 500 多個贊,在 HN 上有 700 多條評論。我們將其翻譯出來,以饗讀者,也希望大家可以理性討論。

在使用 Rust 的過程中,相信很多朋友都有過類似的吐槽:真不確定自己要掌握多少語言知識、多少獨門編程技巧和多麼旺盛的好奇心,才能堅持做完這項最最瑣碎的工作。絕望之下,我們往往會去 rust/issues 尋找解決辦法,然後突然發現了一種在理論上根本不成立的 API 設計思路。這種矛盾源自某種微妙的語言 bug,簡直神奇。

我從四年前開始接觸 Rust。目前爲止,我跟同事們合作開發了 teloxide 和 dptree,寫過相關的書和文章,也翻譯了不少語言發佈的公告。我還設法用 Rust 編寫過一些生產代碼,甚至有幸在一場關注 Rust 的在線研討上發過言。

雖然也算是身經百戰,但我還是動不動就會跟 Rust 的借用檢查器和類型系統 “鬧出” 些不愉快。現在的我,雖然已經慢慢理解了 Rust “無法返回對臨時值的引用”之類的錯誤,也設計出一些啓發式的策略來處理生命週期問題,但最近一個意外再次打擊了我的信心……

初次嘗試:用來處理更新的函數

我們正打算編寫一個聊天機器人,來改善用戶的使用體驗。通過長輪詢或 webhooks,我們開始一個個獲取服務器更新流。我們有一個面向全體更新的處理程序向量,其中每個處理程序都會接收對更新的引用,再把後續解析返回至()。這個處理程序向量由 Dispatcher 所有,每次有更新傳入時,Dispatcher 都會按順序執行各個處理程序。

下面,咱們試試具體實現。這裏省略掉處理程序的執行部分,只關注 push_handler 函數。初次嘗試:省略處理程序的執行,只關注 push_handler 函數。第一次嘗試(遊樂場):

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);
    });
}

在這裏,我們使用由 HRTB 生命週期 for<'a> 限制的動態類型 Fn trait 來表示每個更新處理程序。因爲我們希望返回的 future 由 &'a Update 函數參數中的'a 部分決定。之後,我們又定義了擁有 Vec 的 Dispatcher 類型。

在 push_handler 當中,我們接受一個靜態類型的泛型 H 來返回 Fut;爲了將此類型的值推送至 self.0,我們需要將處理程序打包至新的裝箱處理程序當中,再使用 Box::pin 將返回的 future 轉換爲來自 futures 箱的 BoxFuture。

下面來看看這個解決思路行不行得通:

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 ,也就是我們試圖將 HRTB 的生命週期歸結成 for<'a>。在這種情況下,我們就需要證明 for<'a,'b> 'a:'b (其中'b 爲來自 push_handler 的'a),這顯然不成立。

對於這個問題,我們可以嘗試幾種不同的處理方法:替換掉 Fut 泛型,轉而強制要求 user handler 返回由 for<'a> 限定的 BoxFuture:

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 還是有問題:理想情況下,我們並不希望用戶通過 Box::pin 打包每個處理程序。畢竟 push_handler 纔是專門幹這個的,負責把靜態類型的處理程序轉換成動態類型空間中的等效形式。但如果我們強行要求處理程序保持靜態,又會如何?

要探究答案,我們可以用異構列表來試試。

第二次嘗試:異構列表

異構列表這名稱看着唬人,實際上就是大家熟悉的元組。也就是說,我們需要的是 (H1, H2, H3, ...),其中每個 H 代表不同的處理程序類型。但同時,push_handler 和 execute 操作又要求我們能夠迭代這個元組——單靠原版 Rust 肯定是不行。要達成這個效果,我們就得藉助其他一些神奇的表達機制。

首先來看我們的異構列表表示:

struct Dispatcher<H, Tail> {
    handler: H,
    tail: Tail,
}

struct DispatcherEnd;

是不是有點不知所云?確實如此,我們只是想要構建起 Dispatcher<H1, Dispatcher<H2, Dispatcher<H3, DispatcherEnd>>> 這種形式的類型,其等同於 (H1, H2, H3) 元組。因此,我們現在可以使用簡單的類型歸納來定義 push_handler 函數:

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),
        }
    }
}

有些朋友可能不太熟悉所謂類型級歸納,其實這就是一種常規遞歸,只是適用對象是類型(trait)、而非值:

我們再以同樣的方式實現 execute:

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<'a> 的實現要依賴於具體的'a,而且 dispatcher 還要能夠處理不同生命週期的更新,所以最後一步就是面向全部更新生命週期對 execute 進行抽象:

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;
}

可惜還是行不通:

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`

到這裏,很多朋友應該體會到借用檢查器的可惜之處了吧?而且無論怎麼調整,以上代碼都沒辦法正常編譯。原因如下:傳遞給 dp.push_handler 的閉包接收到一條具體生命週期爲'1 的 upd,但因爲 where 子句中引入了 HRTB 邊界,所以 execute 要求 Dp 只在生命週期'0 上實現 Execute<'0>。但如果我們用常規函數試試運氣,代碼倒是可以正常編譯:

#[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;
}
#[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;
}

這裏會將 Update 傳遞爲標準輸出。

這種借用檢查器的特殊行爲確實不太合理,畢竟函數和閉包不僅各自 trait 不同,而且處理生命週期的方式也有所區別。雖然接受引用的閉包要受到特定生命週期的限制,但像我們使用的 dbg_update 這類函數應該可以在一切生命週期'a 上接受 &'a Update 纔對。以下示例代碼就演示了這種區別:

let dbg_update = |upd| {
    println!("{:?}", upd);
};

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

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

由於調用了 dbg_update,所以我們會得到以下編譯錯誤:

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 閉包只能處理一個特定的生命週期,而第一與第二個 upd 的生命週期顯然並不一樣。

相比之下,作爲函數的 dbg_update 在這裏倒是可以完美運行:

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

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

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

我們甚至可以很方便地使用 let () = ...; 來追蹤該函數的確切簽名:

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

let () = dbg_update_fn;

跟預想的一樣,簽名爲 for<'r> fn(&'r Update):

error[E0308]: mismatched types
 --> src/main.rs: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 `()`

話雖如此,但這樣一個包含異構列表的答案也不符合我們的預期:它太過混亂、僵化、複雜,而且也裝不進閉包。另外,這裏不建議在 Rust 中使用複雜的類型機制。如果大家在處理 dispatcher 類型時突然遇到類型檢查失敗,那麻煩可就大了。想象一下,我們正在維護一套用 Rust 編寫的生產系統,而且需要儘快修復一些關鍵 bug。而在完成了代碼庫的必要更改之後,卻看到了以下編譯輸出:

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

在剛開始接觸 Rust 的時候,我曾經以爲引用要比智能指針更簡單。但現在我基本只用 Rc/Arc 了,畢竟犧牲一點點性能就可以跟生命週期保持距離,這有什麼不好?而且信不信由你,前面提到的所有問題,都是由 type Handler, 'a 中的單一生命週期引起的。

讓我們把它替換成 Arc 的形式:

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);
    });
}

成了,這不就正常編譯了嗎!我們甚至都不需要在每個閉包裏手動指定 Arc——類型推斷就能幫我們完成繁瑣的操作。

Rust 的問題

“隨心所欲地併發” 這話,大家都聽過吧?雖然原則上也沒錯,但這句話其實很有誤導性。沒錯,我們不用再怕數據競爭,可除此之外還有別的麻煩隨之而來。

其實在前文的演示中,我們還沒涉及到 Rust 的全部特性和缺陷,這些毛病其實相當不少。首先就是裝箱 future 的大量使用:之前提到的所有 BoxFuture 類型,以及 Box::new 和 Box::pin 相應的優化,都沒辦法用泛型來替代。

如果大家多少了解一點 Rust,就會知道 Vec 只能容納固定大小的類型,所以才需要把 BoxFuture 放在 type Handler 之內。但在 Execute trait 中使用 BoxFuture(而非 async 函數簽名)時,這個問題就不那麼容易被發現。

這背後的原因也很複雜,但簡單來說就是,我們沒辦法在 traits 中定義 async fn 函數;相反,大家只能使用其他類型擦除方法,例如 async-trait 板條箱或者手動 future 裝箱,也就是我們在示例中採取的辦法。事實上,async-trait 走的也是這個路線,但我還是會盡量少用,因爲它會使用過程宏來處理編譯時錯誤。

另外,返回 BoxFuture 這個辦法也有自己的問題:首先就是我們得牢記爲每個 async fn 指定 #[must_use],否則即使是在沒有. await 的情況下調用 execute,編譯器也不會給出任何警告。從本質上講,裝箱靜態實體實在太多,所以 futures 箱會經常暴露在常見 traits 的動態變體面前,包括 BoxStream, LocalBoxFuture 以及 LocalBoxStream (後兩個不要求 Send)。

其次,upd 的顯式類型註釋又是另一個大問題:

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;
}

編譯器輸出:

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,則編譯成功。)

相信很多朋友都看不懂這裏到底出了什麼錯,這很正常,我們可以參閱一個問題 #70791(https://github.com/rust-lang/rust/issues/70791)。查看問題標籤列表中的 C-Bug,可以看到它將問題歸類爲編譯器 bug。

截至本文撰稿時,rustc 還有 3107 個未解決的 C-bug 問題和 114 個未解決的 C-bug+A-lifttimes 問題。還記得之前提到的 async fn 有效,但等效閉包卻無效的情況嗎?這也是編譯器 bug,具體參考問題 #70263(https://github.com/rust-lang/rust/issues/70263)。

還有更多 2020 年之前就發現的老問題,例如問題 #41078(https://github.com/rust-lang/rust/issues/41078)和問題 #42940(https://github.com/rust-lang/rust/issues/42940)。

另外,就連註冊處理程序這種簡單的任務,我們也得儘量想辦法讓它繞過 Rust 語言,否則就容易受到 rustc 問題的影響。在 Rust 中設置接口就像是趟雷區:要想成功,就得小心翼翼在理想接口和可用功能之間求取平衡。

有些朋友可能要說,編程語言不都這樣嗎?那可不是,Rust 的問題特殊得多、也煩人得多。

我們在使用其他穩定生產語言的時候,一般至少可以預判理想中的接口要如何適應語言語義,但在用 Rust 編程時,設計 API 的過程總會受到語言自身的種種限制和影響。剛開始,我們當然想正常通過借用檢查器驗證引用,用類型機制處理程序實體,但最終結果永遠是 Box、Pin 和 Arc 滿天飛、身陷 Rust 類型系統那孱弱的表達能力難以自拔。

作爲這一段的結尾,我們來看同樣的需求在 Golang 中的實現方法:

dispatcher.go

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 爲什麼這麼難用?

首先,面對這類問題的時候,希望大家能拋掉 “因爲 XX 就是遜啦” 或者 “因爲 XX 的設計者太弱智” 這類粗暴又毫無意義的情緒宣泄。

那麼,Rust 爲什麼這麼難用?

首先,Rust 是一種系統語言。作爲系統編程語言,Rust 絕對不能阻止程序員直接接觸底層計算機內存的管理機制。也正因爲如此,Rust 才向程序員們開放了其他高級語言所極力隱藏的種種細節。例如:指針、引用和相關等元素,內存分配器、不同字符串類型、各種 Fn trats、std::pin 板條箱等等。

其次,Rust 是一種靜態語言。具有靜態類型系統(或等效功能)的語言,更傾向於在靜態和動態層級上覆制功能,藉此引入靜態 - 動態二元性。將靜態抽象轉換爲動態對應抽象,被稱爲向上轉換;由動態轉換到靜態則稱爲向下轉換。在 push_handler 當中,我們使用向上轉換將靜態處理程序轉換爲動態 Handler 類型,再把它推送給最終向量。

另外,Rust 在設計上還高度強調直觀性和內存安全性。正是這種複雜的組合,在計算機語言的設計中強調了人爲邊界的重要性。

說到這裏,大家應該能夠理解爲什麼 Rust 用起來總感覺哪裏有毛病。事實上,它能運行起來本身就已經是個奇蹟了。計算機語言是一種由無數組件緊密交織而成的體系:每當引入新的語言抽象時,都得保證它能跟系統的其餘部分良好配合,避免引發 bug 或不一致。所以,我們真的應該感謝和敬佩那些願意全職開發這類語言的貢獻者,最好能給他們捐點款。

Rust 還能不能變得更好?

現在,我們假設 Rust 的所有問題一夜之間都被解決了,而且整個 rustc 和 std 也都經過了正式驗證。就是說,Rust 突然就獲得了包含多個 1 級實現的完整語言規範、能夠跟 GCC 比肩的硬件平臺支持能力、穩定的 ABI(雖然還不清楚具體該怎麼處理泛型),結果會怎麼樣?那 Rust 當然就是系統編程的理想語言嘍。

我們也可以從另一個角度設想,Rust 的問題確實消失了,而且變成了一種徹頭徹尾的高級語言。那它就足以幹掉一切現有主流編程語言。畢竟 Rust 的默認功能相當豐富,支持多態,包管理器也非常方便。相比之下,愚蠢的 JavaScript 語義、恐怖的 Java 企業應用、C 中的 NULL 指針問題、C++ 的不可控 UB、C# 中多到毫無必要的同種功能實現等等,簡直就是一場荒謬的畸形秀。

但現實告訴我們,即使這些語言各自有着不同的缺點,人們仍然用它編寫生產軟件,而且當前的 Rust 還遠遠擠不進編程語言的第一梯隊。

另外,我估計 Rust 永遠也達不到 Java 或者 Python 那樣的人氣。這背後的原因更多在於社區、而非技術:由於 Rust 語言天生更爲複雜,所以相關的專業工程師數量不可能比得上 Java 或者 Python。

更糟糕的是,Rust 工程師的稀缺也讓他們的平均薪酬相對更高。畢竟作爲企業僱主,確實沒必要用更多的錢和更長的招聘週期來無腦支持 Rust。

最後再做這樣的設想:Rust 的問題全都消失了,它變成了一套高級且統一的功能集。這可能也是 Rust 開發者們的終極目標:讓它成爲一種面向大衆的高級泛化編程語言。有趣的是,設計這樣一種語言可能反而比開發現有 Rust 的難度更低,畢竟我們可以把所有低級細節都隱藏在那層厚厚的語言運行時外殼之下。

好日子會到來的

所以我好像突然想通了,爲什麼不開發這樣一個終極版的 Rust 呢?但我可不打算親自動手,畢竟這工作沒準得耗上十年、二十年,而且最終成果能在編程語言中脫穎而出的幾率也實在不高。

在我看來,目前常用生產語言的成功其實有着很強的隨機性——我們雖然能從特定的流行語言身上總結出一些明確的優勢,但卻沒法解釋爲什麼其他一些更好的替代語言始終默默無聞。有大企業的支持?無意中契合了下一階段的 IT 發展趨勢?但大企業爲什麼要支持,這種契合又是怎麼達成的?殘酷的現實告訴我們:有些事情就是隨機發生,再強的技能、再無私的奉獻都改變不了。

所以如果大家也想創造一種面向未來的編程語言,我建議大家三思而後行——除非你既勇猛無比,又是個無可救藥的理想主義者。

作者的一點澄清

有人指出,dispatcher 例子主要影響的是庫維護者羣體,應用程序開發者一般不會受到這類特殊問題的影響。這話其實沒毛病,但我寫這篇文章主要是想討論 Rust 語言的設計思路。

Rust 並不適合泛型 async 編程,這是事實。當我們輸入 async 時,總會觀察到語言中的其他功能突然崩潰:引用、閉包、類型系統等等。從語言設計的角度來看,這正好體現了 Rust 完全不符合 “正交語言”(如果在編程時調用程序語言不用考慮其是否影響其它語言特性,就稱此語言爲正交程序語言)的基本原則。我在原文中想要表達的,其實就是這個意思。

另外,能不能編寫出高質量的庫,在很大程度上反映出了語言的真正潛力。畢竟庫的任務就是處理泛化代碼,所以直接對應語言設計得提供的功能表示能力。這種能力也會直接影響到常規應用編程:我們的庫越優雅,日常任務的解決難度就越低。例如:不具備 GATs,我們就無法獲得泛化運行時接口,並只通過一行代碼就直接把日誌中的 Tokio 全部替換爲正確的 Tokyo。

另一位熱心的朋友還整理出一份完整的 async Rust 故障列表(https://www.reddit.com/r/rust/comments/v3cktw/comment/ib0mp49/?utm_source=share&utm_medium=web2x&context=3),其中包括函數着色、異步 Drop 和庫代碼重複等問題。

受篇幅所限,我在本文中無法一一列舉這些內容。但在使用 Rust 之前,建議大家想看看要使用泛化異步代碼時可能面對的種種問題,別被嚇着哦:)

原文鏈接:

https://hirrolot.github.io/posts/rust-is-hard-or-the-misery-of-mainstream-programming.html

作者:Hirrolot

來源:InfoQ

技術最前線 關注 IT 業界的新技術和新動態

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/SeS3HpZS11XbZ9eqVIPwEw