深入淺出Rust Future - Part 1

本文譯自Rust futures: an uneducated, short and hopefully not boring tutorial - Part 1,時間:2018-12-02,譯者:
motecshine, 簡介:motecshine

歡迎向Rust中文社區投稿,投稿地址,好文將在以下地方直接展示

  1. Rust中文社區首頁
  2. Rust中文社區Rust文章欄目
  3. 知乎專欄Rust語言
  4. sf.gg專欄Rust語言

Intro

如果你是一個程序員並且也喜歡Rust這門語言, 那麼你應該經常在社區聽到討論Future 這個庫的聲音, 一些很優秀的Rust Crates都使用了Future 所以我們也應該對它有足夠的瞭解並且使用它. 但是大多數程序員很難理解Future到底是怎麼工作的, 當然有官方 Crichton's tutorial這樣的教程, 雖然很完善, 但我還是很難理解並把它付諸實踐.

我猜測我並不是唯一一個遇到這樣問題的程序員, 所以我將分享我自己的最佳實踐, 希望這能幫助你理解這個話題.

Futures in a nutshell

Future 是一個不會立即執行的特殊functions. 他會在將來執行(這也是他被命名爲future的原因).我們有很多理由讓future functions來替代std functions,例如: 優雅性能可組合性.future的缺點也很明顯: 很難用代碼去實現. 當你不知道何時會執行某個函數時, 你又怎麼能理解他們之間的因果關係呢?

處於這個原因, Rust會試圖幫助我們這些菜鳥程序員去理解和使用future這個特性。

Rust's futures

Rust 的futures 總是一個Results: 這意味着你必須同時指定期待的返回值和備用的錯誤類型。 讓我們先簡單的實現一個方法,然後把它改造成future. 我們設計的這個方法返回值是 u32 或者是一個 被Box包圍着的Error trait, 代碼如下所示:

fn my_fn() -> Result<u32, Box<Error>> { 
    Ok(100) 
} 

這段代碼很簡單,看起來並沒有涉及到future. 接下來讓我們看看下面的代碼:

fn my_fut() -> impl Future<Item = u32, Error = Box<Error>> { 
    ok(100) 
} 

注意這兩段代碼不同的地方:

  1. 返回的類型不再是Result而是一個impl Future. Rust Nightly版本是允許我們返回一個future的。
  2. 第二個函數返回值的參量Item = u32, Error = Box<Error>較第一個函數來看更加詳細明確。
爲了能讓第二段代碼工作 你需要使用擁有conservative_impl_trait特性的nightly版本。當然,如果不嫌麻煩,你可以使用boxed trait來替代。

另請注意第一個函數返回值使用的是大寫的Ok(100)。 在Result函數中,我們使用大寫的Ok枚舉,而future我們使用小寫的ok方法.

規則: 在Rustfuture中使用小寫返回方法ok(100).

好了現在我們改造完畢了,但是我們該怎樣執行第二個我們改造好的方法?標準方法我們可以直接調用,但是這裏需要注意的是地一個方法返回值是一個Result, 所以我們需要使用unwrap()來獲取我們期待的值。

let retval = my_fn().unwrap(); 
println!("{:?}", retval); 

由於future在實際執行之前返回(或者更準確的說, 返回的是我們將來要執行的代碼), 我們需要一種途徑去執行future。爲此我們使用Reactor。我們只需要創建一個Reactor並且調用他的run方法就可以執行future. 就像下面的代碼:

let mut reactor = Core::new().unwrap(); 
let retval = reactor.run(my_fut()).unwrap(); 
println!("{:?}", retval); 

注意這裏我們unwrap的是run方法,而不是my_fut. 看起來真的很簡單。

Chaining

future一個很重要的特性就是能夠把其他的future組織起來形成一個chain. 舉個栗子:

你邀請你的父母一起吃晚飯通過email.
你在電腦前等待他們的回覆
父母同意與你一起吃晚飯(或者因爲一些原因拒絕了)。

Chaining就是這樣的,讓我們看一個簡單的例子:

fn my_fn_squared(i: u32) -> Result<u32, Box<Error>> { 
     Ok(i * i) 
} 

fn my_fut_squared(i: u32) -> impl Future<Item = u32, Error = Box<Error>> { 
    ok(i * i) 
} 

現在我們可以使用下面的方式去調用這兩個函數:

let retval = my_fn().unwrap(); 
println!("{:?}", retval); 
let retval2 = my_fn_squared(retval).unwrap(); 
println!("{:?}", retval2); 

當然我們也可以模擬Reactor來執行相同的代碼:

let mut reactor = Core::new().unwrap(); 
let retval = reactor.run(my_fut()).unwrap(); 
println!("{:?}", retval); 
let retval2 = reactor.run(my_fut_squared(retval)).unwrap(); 
println!("{:?}", retval2); 

但還有更好的方法,在Rust中future也是一個trait他有很多種方法(這裏我們會介紹些),其中一個名爲and_then的方法,在語義上完全符合我們最後寫的代碼片段。但是沒有顯式的執行Reactor Run, 讓我們一起來看看下面的代碼:

let chained_future = my_fut().and_then(|retval| my_fut_squared(retval));
let retval2 = reactor.run(chained_future).unwrap(); 
println!("{:?}", retval2); 

讓我們看看第一行:創建一個被叫做chained_futurefuture, 它把my_futmu_fut_squared`future串聯了起來。 這裏讓人難以理解的部分是: 我們如何將上一個future的結果傳遞給下一個future`?

在Rust中我們可以通過閉包來捕獲外部變量來傳遞future的值。 可以這樣想:
  1. 調度並且執行my_fut()
  2. my_fut()執行完畢後,創建一個retval變量並且將my_fut()的返回值存到其中。
  3. 現在將retval作爲my_fn_squared(i: u32)的參數傳遞進去,並且調度執行my_fn_squared
  4. 把上面一些列的操作打包成一個名爲chained_future的調用鏈。

第二行代碼,與之前的相同: 我們調用Reactor run(), 要求執行chained_future並給出結果。 當然我們可以通過這種方式將無數個future打包成一個chain, 不要去擔心性能問題, 因爲future chainzero cost.

RUST borrow checked可能讓你的future chain 寫起來不是那麼的輕鬆,所以你可以嘗試move你的參數變量.

Mixing futures and plain functions

你也可以使用普通的函數來做future chain, 這很有用, 因爲不是每個功能都需要使用future. 此外, 你也有可能希望調用外部你無法控制的函數。 如果函數沒有返回Result,你只需在閉包中添加函數調用即可。 例如,如果我們有這個普通函數:

fn fn_plain(i: u32) -> u32 { 
    i - 50  
} 

let chained_future = my_fut().and_then(|retval| { 
    let retval2 = fn_plain(retval); 
    my_fut_squared(retval2) 
}); 
let retval3 = reactor.run(chained_future).unwrap(); 
println!("{:?}", retval3); 

如果你的函數返回Result則有更好的辦法。我們一起來嘗試將my_fn_squared(i: u32) -> Result<u32, Box<Error>方法打包進future chain

在這裏由於返回值是Result所以你無法調用and_then, 但是future有一個方法done()可以將Result轉換爲impl Future.這意味着我們可以將普通的函數通過done方法把它包裝成一個future.

let chained_future = my_fut().and_then(|retval| { 
    done(my_fn_squared(retval)).and_then(|retval2| my_fut_squared(retval2)) 
}); 
let retval3 = reactor.run(chained_future).unwrap(); 
println!("{:?}", retval3); 

注意第二:done(my_fn_squared(retval))允許我們在鏈式調用的原因是:我們將普通函數通過done方法轉換成一個impl Future. 現在我們不使用done方法試試:

let chained_future = my_fut().and_then(|retval| {
    my_fn_squared(retval).and_then(|retval2| my_fut_squared(retval2)) 
}); 
let retval3 = reactor.run(chained_future).unwrap(); 
println!("{:?}", retval3); 

編譯不通過!

Compiling tst_fut2 v0.1.0 (file:///home/MINDFLAVOR/mindflavor/src/rust/tst_future_2) 
error[E0308]: mismatched types 
--> src/main.rs:136:50 | 136 | my_fn_squared(retval).and_then(|retval2| my_fut_squared(retval2)) | ^^^^^^^^^^^^^^^^^^^^^^^ expected enum `std::result::Result`, found anonymized type | = note: expected type `std::result::Result<_, std::boxed::Box<std::error::Error>>` found type `impl futures::Future` 
error: aborting due to previous error 
error: Could not compile `tst_fut2`. 

expected type std::result::Result<_, std::boxed::Box<std::error::Error>> found type impl futures::Future,這個錯誤有點讓人困惑. 我們將會在第二部分討論它。

Generics

最後但並非最不重要的, futuregeneric(這是啥玩意兒啊)一起工作不需要任何黑魔法.

fn fut_generic_own<A>(a1: A, a2: A) -> impl Future<Item = A, Error = Box<Error>> where A: std::cmp::PartialOrd, { 
    if a1 < a2 { ok(a1) } else { ok(a2) } 
} 

這個函數返回的是 a1 與 a2之間的較小的值。但是即便我們很確定這個函數沒有錯誤也需要給出Error,此外,返回值在這種情況下是小寫的ok(原因是函數, 而不是enmu)

現在我們調用這個future:

let future = fut_generic_own("Sampdoria", "Juventus"); 
let retval = reactor.run(future).unwrap(); 
println!("fut_generic_own == {}", retval); 

閱讀到現在你可能對future應該有所瞭解了, 在這邊文章裏你可能注意到我沒有使用&, 並且僅使用函數自身的值。這是因爲使用impl Future,生命週期的行爲並不相同,我將在下一篇文章中解釋如何使用它們。在下一篇文章中我們也會討論如何在future chain處理錯誤和使用await!()宏。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章