給 Ruby 開發者的 Rust
最近我讀到一篇關於慣用 Ruby 寫法的精彩文章。我絕對稱不上是優秀的 Ruby 開發者,但我發現其中有許多模式在 Rust 中也相當常見。接下來將並列比較兩種語言中的慣用寫法。
Ruby 程式碼範例取自原文。
Map 與 Higher-Order Functions(高階函式)
第一個範例是使用 map 對容器中的元素進行相當基礎的迭代。
user_ids = users.map { |user| user.id }map 的概念在 Rust 中也相當標準。相較於 Ruby,我們在這裡需要寫得更明確一些:如果 users 是由 User 物件組成的 vector,我們首先需要從中建立一個 iterator:
let user_ids = users.iter().map(|user| user.id);你可能會說這樣相當冗長,但這個額外的抽象讓我們得以表達一個重要的概念:這個 iterator 會不會取得 vector 的所有權?
- 使用
iter()時,你會得到 vector 的「唯讀視圖」。迭代結束後,它不會有任何改變。 - 使用
into_iter()時,你會取得 vector 的所有權。迭代結束後,該 vector 就會消失。以 Rust 的術語來說,它已經 moved(被移動)。 - 在此處閱讀更多關於
iter()與into_iter()差異的說明。
上述 Ruby 程式碼可以簡化如下:
user_ids = users.map(&:id)在 Ruby 中,Higher-Order Functions(高階函式)(例如 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 中,higher-order functions 會將函式作為參數。因此,users.iter().map(Users::id) 大致上等同於 users.iter().map(|u| u.id())。2
此外,Rust 中的 map() 回傳的是另一個 iterator,而非集合。如果你想要一個集合,就必須對其執行 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();我在這裡改了兩個地方:
- 我現在使用的是
filter_map,而非map。 parse回傳的是Result,但filter_map預期的是Option。我們可以透過對其呼叫ok()將Result轉換為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"這可以像這樣翻譯:
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 的指正。↩
隨機一篇部落格