給 Ruby 開發者的 Rust
原文由 Matthias Endler 于 發布,訂閱此部落格
最近我讀到一篇談論道地 Ruby 寫法的精彩文章。我絕對稱不上是優秀的 Ruby 開發者,但我發現其中有不少模式在 Rust 中也很常見。接下來將並列比較兩種語言中符合慣例的寫法。
Ruby 的程式碼範例取自原文。
Map 與高階函式
第一個範例是使用 map 對容器中的元素進行遍歷,算是相當基礎的操作。
user_ids = users.map { |user| user.id }map 的概念在 Rust 中也相當常見。相較於 Ruby,這裡我們需要寫得更明確一些:如果 users 是一個由 User 物件組成的 vector,我們得先從它建立一個迭代器:
let user_ids = users.iter().map(|user| user.id);你可能會覺得這樣有點冗長,但這層額外的抽象讓我們得以表達一個重要的概念:這個迭代器究竟會不會取得該 vector 的所有權?
- 使用
iter()時,你得到的是該 vector 的「唯讀視圖」。遍歷結束後,vector 本身保持不變。 - 使用
into_iter()時,你會取得該 vector 的所有權。遍歷結束後,這個 vector 就不存在了。用 Rust 的術語來說,它已經 moved(被移動)了。 - 想進一步了解
iter()與into_iter()之間的差異,請看這裡。
上面的 Ruby 程式碼還可以簡化成這樣:
user_ids = users.map(&:id)在 Ruby 中,高階函式(例如 map)會以區塊(block)或 proc 作為參數,而語言本身提供了一種呼叫方法的便捷寫法——&:id 就等同於 {|o| o.id()}。
在 Rust 中也可以做到類似的事:
let id = |u: &User| u.id;
let user_ids = users.iter().map(id);不過,這大概不是最符合慣例的寫法。更常見的做法是在這種情況下使用通用函式呼叫語法(Universal Function Call Syntax):1
let user_ids = users.iter().map(User::id);在 Rust 中,高階函式是以 函式 作為參數。因此 users.iter().map(Users::id) 大致上等同於 users.iter().map(|u| u.id())。2
另外,Rust 中的 map() 回傳的是另一個迭代器,而不是集合。如果你想要得到集合,就必須對其執行collect(),這點我們稍後會看到。
以 Each 進行迭代
說到迭代,我在 Ruby 程式碼中很常看到這樣的寫法:
["Ruby", "Rust", "Python", "Cobol"].each do |lang|
puts "Hello #{lang}!"
end自 Rust 1.21 起,這在 Rust 中也做得到了:
["Ruby", "Rust", "Python", "Cobol"]
.iter()
.for_each(|lang| println!("Hello {lang}!", lang = lang));不過,在 Rust 中更常見的寫法是使用一般的 for 迴圈:
for lang in ["Ruby", "Rust", "Python", "Cobol"].iter() {
println!("Hello {lang}!", lang = lang);
}Select 與 filter
假設你想在 Ruby 中從一個集合裡只取出偶數。
even_numbers = [1, 2, 3, 4, 5].map { |element| element if element.even? } # [ni, 2, nil, 4, nil]
even_numbers = even_numbers.compact # [2, 4]在這個範例中,呼叫 compact 之前,我們的 even_numbers 陣列中含有 nil。而在 Rust 中,並沒有 nil 或 Null 的概念。你不需要 compact。而且,map 也不接受述詞(predicate)。這種情況你應該使用 filter:
let even_numbers = vec![1, 2, 3, 4, 5]
.iter()
.filter(|&element| element % 2 == 0);或者,若想將結果轉成 vector:
// Result: [2, 4]
let even_numbers: Vec<i64> = vec![1, 2, 3, 4, 5]
.into_iter()
.filter(|element| element % 2 == 0).collect();一些補充說明:
- 我在這裡加上了型別提示
Vec<i64>,因為如果沒有它,Rust 在呼叫collect時就無法知道我想建立哪種集合。 vec!是用來建立 vector 的巨集。- 我在這裡使用的是
into_iter而非iter。這樣我就能取得 vector 中元素的所有權。如果使用iter(),得到的則會是Vec<&i64>。
在 Rust 中,數值本身並沒有 even 方法,但這不妨礙我們自己定義一個!
let even = |x: &i64| x % 2 == 0;
let even_numbers = vec![1, 2, 3, 4, 5].into_iter().filter(even);在實際的專案中,你可能會使用像 num 這樣的第三方套件(crate)來處理數值運算:
extern crate num;
use num::Integer;
fn main() {
let even_numbers: Vec<i64> = vec![1, 2, 3, 4, 5]
.into_iter()
.filter(|x| x.is_even()).collect();
}一般來說,在 Rust 中對於標準函式庫沒有提供的功能,使用 crate 是相當常見的做法。會如此普及,部分原因是 cargo 是一個非常出色的套件管理器。(或許是因為它正是由 Ruby 界大名鼎鼎的 Yehuda Katz 所打造的也說不定。 😉)
如前所述,Rust 並沒有 nil。不過,仍然有「可能失敗的操作」這樣的概念。用來表達這種情況的標準型別叫做 Result。
假設你想把一個字串 vector 轉換成整數。
let maybe_numbers = vec!["1", "2", "nah", "nope", "3"];
let numbers: Vec<_> = maybe_numbers
.into_iter()
.map(|i| i.parse::<u64>())
.collect();這樣看起來不錯,但輸出結果可能會有些出乎意料。numbers 裡面也會包含解析失敗的錯誤:
[Ok(1), Ok(2), Err(ParseIntError { kind: InvalidDigit }), Err(ParseIntError { kind: InvalidDigit }), Ok(3)]有時候你只關心成功的轉換。要過濾掉錯誤,一個簡單的方法是使用 filter_map:
let maybe_numbers = vec!["1", "2", "nah", "nope", "3"];
let numbers: Vec<_> = maybe_numbers
.into_iter()
.filter_map(|i| i.parse::<u64>().ok())
.collect();我在這裡改了兩個地方:
- 原本使用
map,現在改用filter_map。 parse回傳的是Result,但filter_map預期的是Option。我們可以透過對Result呼叫ok()來將其轉換為Option3。
回傳值包含了所有成功轉換的字串:
[1, 2, 3]filter_map 類似於 Ruby 中的 select 方法:
[1, 2, 3, 4, 5].select { |element| element.even? }隨機數
在 Ruby 中,要從陣列中取得一個隨機數可以這樣寫:
[1, 2, 3].sample這種寫法相當簡潔且符合慣例!來對比一下 Rust 的寫法:
let mut rng = thread_rng();
rng.choose(&[1, 2, 3, 4, 5])要讓這段程式碼能執行,你需要 rand 這個 crate。點擊程式碼片段可查看可執行的範例。
和 Ruby 相比有一些差異。也就是說,我們必須更明確地指定究竟想要哪一種隨機數產生器。在這裡,我們選擇了一個由系統播種、延遲初始化的執行緒區域隨機數產生器。在這個例子中,我使用的是 slice 而非 vector。主要差別在於 slice 的大小是固定的,而 vector 則不是。
在標準函式庫中,Rust 的 slice 本身並沒有 sample 或 choose 方法。這是一個設計上的抉擇:語言的核心保持精簡,以利未來持續演進。
但這不代表你現在就無法擁有更優雅的實作。舉例來說,你可以定義一個 Choose trait,並為 [T] 實作它。
extern crate rand;
use rand::{thread_rng, Rng};
trait Choose<T> {
fn choose(&self) -> Option<&T>;
}
impl<T> Choose<T> for [T] {
fn choose(&self) -> Option<&T> {
let mut rng = thread_rng();
rng.choose(&self)
}
}這些樣板程式碼可以放進一個 crate 中,讓其他人重複使用。有了它,我們就能得到一個足以媲美 Ruby 優雅程度的解法。
[1, 2, 4, 8, 16, 32].choose()隱式回傳與表達式
Ruby 的方法會自動回傳最後一個陳述式的結果。
def get_user_ids(users)
users.map(&:id)
endRust 也是如此。請注意結尾缺少了分號。
fn get_user_ids(users: &[User]) -> Vec<u64> {
users.iter().map(|user| user.id).collect()
}但在 Rust 中,這僅僅是個開始,因為所有東西都是表達式。下面的區塊會將字串拆成字元,移除其中的 h,並將結果以 HashSet 的形式回傳。這個 HashSet 將會被指派給 x。
let x: HashSet<_> = {
// Get unique chars of a word {'h', 'e', 'l', 'o'}
let unique = "hello".chars();
// filter out the 'h'
unique.filter(|&char| char != 'h').collect()
};條件判斷也是一樣:
let x = if 1 > 0 { "absolutely!" } else { "no seriously" };由於 match 陳述式也是一種表達式,你同樣可以將其結果指派給變數!
enum Unit {
Meter,
Yard,
Angstroem,
Lightyear,
}
let length_in_meters = match unit {
Unit::Meter => 1.0,
Unit::Yard => 0.91,
Unit::Angstroem => 0.0000000001,
Unit::Lightyear => 9.461e+15,
};多重指派
在 Ruby 中,你可以一次性將多個值指派給多個變數:
def values
[1, 2, 3]
end
one, two, three = values在 Rust 中,你只能將 tuple 解構為 tuple,舉例來說,無法將 vector 解構為 tuple。所以這樣寫是可行的:
let (one, two, three) = (1, 2, 3);但這樣就不行:
let (one, two, three) = [1, 2, 3];
// ^^^^^^^^^^^^^^^^^ expected array of 3 elements, found tuple這樣也不行:
let (one, two, three) = [1, 2, 3].iter().collect();
// a collection of type `(_, _, _)` cannot be built from an iterator over elements of type `&{integer}`但使用 nightly 版的 Rust,現在你可以這樣寫:
let [one, two, three] = [1, 2, 3];另一方面,除了多重指派之外,透過解構你還能做更多事。你可以運用模式語法寫出優美且符合人體工學的程式碼。
let x = 4;
let y = false;
match x {
4 | 5 | 6 if y => println!("yes"),
_ => println!("no"),
}這會印出
no,因為 if 條件是套用於整個模式4 | 5 | 6,而不僅僅是最後一個值 6。
字串插值
Ruby 擁有強大的字串插值支援。
programming_language = "Ruby"
"#{programming_language} is a beautiful programming language"這可以這樣轉換成 Rust:
let programming_language = "Rust";
format!("{} is also a beautiful programming language", programming_language);具名參數也是可行的,儘管比較少見:
println!("{language} is also a beautiful programming language", language="Rust");Rust 的 println!() 語法甚至比 Ruby 還要豐富。如果你好奇還能做些什麼,可以查閱文件。
就是這樣!
Ruby 為許多常見的使用模式提供了語法糖,讓程式碼可以寫得非常優雅。低階程式設計與極致效能並非該語言的主要目標。
如果你確實需要這些,Rust 或許會是個不錯的選擇,因為它在提供精細的硬體控制的同時,也具備相當的人體工學設計。不過,如有疑慮,Rust 更傾向於明確性;它排斥魔法般的隱晦行為。
是否已經勾起你對道地 Rust 寫法的興趣了呢?不妨看看這個 GitHub 專案。若能獲得你的貢獻,我將不勝感激。
註腳
1. 感謝 Florian Gilcher 的提醒。↩
2. 感謝 masklin 指出多處不準確之處。↩
3. 在第一版中,我曾說 ok() 會將 Result 轉換為 boolean,這是錯誤的。感謝 isaacg 的指正。↩
隨機一篇部落格
留言
登入後參與討論