Rust for Rubyists

Matthias Endler

RubyistのためのRust

原文は Matthias Endler により に公開されました。 このブログを購読する

最近、イディオマティックなRubyについての素敵な記事を見つけた。決して優れたRuby開発者ではないのだが、そこで紹介されているパターンの多くはRustでもよく見られることに気づいた。以下では、両言語におけるイディオマティックなコードを並べて比較してみる。

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の用語で言えば、ムーブされたことになる。
  • 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);

ただし、これはおそらく最もイディオマティックなやり方ではない。こうした場合によりよく見かけるのは、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は述語を取らない。そのような場合には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_mapOptionを期待する。Resultに対してok()を呼び出すことで、ResultOptionに変換できる3

戻り値には、変換に成功した文字列だけが含まれる。

[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クレートが必要だ。実行例はスニペットをクリックしてほしい。

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)
    }
}

このボイラープレートをクレートに入れて、他の人にも再利用できるようにできる。そうすれば、Rubyのエレガンスに匹敵する解決策にたどり着ける。

[1, 2, 4, 8, 16, 32].choose()

暗黙のreturnと式

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では複数の値を一挙に変数に代入できる。

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には多くの一般的な使用パターンに対するシンタックスシュガーが用意されており、非常にエレガントなコードを書くことができる。低レベルプログラミングや生のパフォーマンスは、この言語の主目的ではない。

もしそれらが必要なのであれば、Rustが良い選択肢になるかもしれない。Rustは同等のエルゴノミクスを保ちつつ、きめ細かいハードウェア制御を提供するからだ。もっとも、迷ったときはRustは明示性を好む。魔法のような挙動は避けるのだ。

イディオマティックなRustに興味が湧いただろうか。こちらのGitHubプロジェクトをぜひ見てほしい。コントリビューションも大歓迎だ。

脚注

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

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。

コメント