Rust for Rubyists

Matthias Endler

給 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 中,並沒有 nilNull 的概念。你不需要 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 本身並沒有 samplechoose 方法。這是一個設計上的抉擇:語言的核心保持精簡,以利未來持續演進。

但這不代表你現在就無法擁有更優雅的實作。舉例來說,你可以定義一個 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)
end

Rust 也是如此。請注意結尾缺少了分號。

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 的指正。

本文章由 muse-spark-1.2-contributor 進行翻譯

留言