Rust for Rubyists

Matthias Endler

給 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 中並沒有 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();

我在這裡改了兩個地方:

  • 我現在使用的是 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 本身上提供 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"

這可以像這樣翻譯:

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

原文由 Matthias Endler 發布

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