Rust for Rubyists

Matthias Endler

RubyistのためのRust

最近、Rubyらしいコードの書き方について書かれた、とても面白い記事を見つけました。私は決してRubyが得意な開発者ではありませんが、そこで紹介されているパターンの多くはRustでもかなり一般的だと気づきました。ここでは、両方の言語で idiomatic なコードを横並びで比較していきます。

Rubyのコード例は元の記事から引用しています。

mapと高階関数

最初の例は、コンテナの要素をmapで反復処理する、ごく基本的なものです。

user_ids = users.map { |user| user.id }

mapという考え方は、Rustでもごく標準的なものです。Rubyと比べると、ここでは少し明示的に書く必要があります。usersUserオブジェクトのベクターなら、まずそこからイテレーターを作ります。

let user_ids = users.iter().map(|user| user.id);

かなり冗長だと思うかもしれません。しかし、この追加の抽象化によって重要な概念を表現できます。つまり、イテレーターがベクターの所有権を取得するのか、それともしないのか、ということです。

  • iter()を使うと、ベクターを「読み取り専用で見る」ことができます。反復処理の後も、ベクターは変更されません。
  • into_iter()を使うと、ベクターの所有権を取得します。反復処理の後、ベクターはなくなります。Rustの用語では、moveされたことになります。
  • iter()into_iter()の違いについてはこちらも読んでみてください。

先ほどのRubyコードは、次のように簡略化できます。

user_ids = users.map(&:id)

Rubyでは、高階関数(mapなど)はブロックやprocを引数に取ります。また、メソッド呼び出しの便利な省略記法も用意されています。&:id{|o| o.id()}と同じです。

Rustでも似たことができます。

let id = |u: &User| u.id;
let user_ids = users.iter().map(id);

ただし、これはおそらく最もidiomaticな書き方ではありません。実際には、この場合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以降では、これも可能になりました。

["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は述語を受け取りません。その場合はfilterを使います。

let even_numbers = vec![1, 2, 3, 4, 5]
    .iter()
    .filter(|&element| element % 2 == 0);

あるいは、結果からベクターを作るなら次のようにします。

// Result: [2, 4]
let even_numbers: Vec<i64> = vec![1, 2, 3, 4, 5]
    .into_iter()
    .filter(|element| element % 2 == 0).collect();

いくつか補足します。

  • ここでVec<i64>という型ヒントを使っているのは、これがないと、collectを呼び出したときにRustがどのコレクションを作ればよいのかわからないためです。
  • vec!はベクターを作成するマクロです。
  • iterの代わりにinto_iterを使っています。これにより、ベクター内の要素の所有権を取得します。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です。

文字列のベクターを整数に変換したいとします。

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();

ここでは2つ変更しています。

  • mapの代わりにfilter_mapを使っています。
  • parseResultを返しますが、filter_mapが要求するのはOptionです。Resultok()を呼び出せば、ResultOptionに変換できます。3

戻り値には、変換に成功した文字列がすべて含まれます。

[1, 2, 3]

Rustのfilter_mapは、Rubyのselectメソッドに似ています。

[1, 2, 3, 4, 5].select { |element| element.even? }

乱数

Rubyで配列からランダムに値を一つ取り出す方法は次のとおりです。

[1, 2, 3].sample

とても簡潔で、いかにもRubyらしいですね。これをRustと比べてみましょう。

let mut rng = thread_rng();
rng.choose(&[1, 2, 3, 4, 5])

このコードを動かすには、rand crateが必要です。スニペットをクリックすると、実行例を確認できます。

Rubyとはいくつか違いがあります。具体的には、どの乱数生成器を使うのかを正確に指定する必要があります。ここでは、システムによってシードが設定され、遅延初期化されるスレッドローカルな乱数生成器を選んでいます。また、この例ではベクターではなくスライスを使っています。主な違いは、スライスのサイズが固定されているのに対し、ベクターのサイズは固定されていないことです。

Rustの標準ライブラリには、スライス自体のメソッドとしてsamplechooseは用意されていません。これは設計上の判断です。将来にわたって言語を発展させられるよう、言語のコアを小さく保っているのです。

だからといって、今すぐより使いやすい実装を用意できないわけではありません。たとえばChooseトレイトを定義し、[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として返しています。このHashSetxに代入されます。

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では、1回の操作で複数の値を変数に代入できます。

def values
  [1, 2, 3]
end

one, two, three = values

Rustでは、タプルを分解してタプルにすることはできますが、たとえばベクターをタプルに分解することはできません。したがって、これは動きます。

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条件は最後の値である6だけでなく、パターン全体の4 | 5 | 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には、よくある利用パターン向けのシンタックスシュガーが数多く用意されているため、とても elegant なコードを書けます。低レベルプログラミングや生の実行性能は、この言語の主な目標ではありません。

もしそれが必要なら、Rustはよい選択肢になるかもしれません。Rustは、同程度に使いやすい記法で、ハードウェアをきめ細かく制御できるからです。ただし、迷ったときにRustが選ぶのは明示性です。魔法には頼りません。

idiomaticなRustに興味が湧いてきましたか。こちらのGitHubプロジェクトをご覧ください。貢献していただけるとうれしいです。

脚注

1. ヒントをくれたFlorian Gilcherに感謝します。
2. 複数の誤りを指摘してくれたmasklinに感謝します。
3. 初版では、ok()Resultbooleanに変換すると書いていましたが、これは誤りでした。訂正してくれたisaacgに感謝します。

原文は Matthias Endler により に公開されました。

この記事は「gpt-5.6-luna」を使用して翻訳されました。