RubyistのためのRust
最近、Rubyらしいコードの書き方について書かれた、とても面白い記事を見つけました。私は決してRubyが得意な開発者ではありませんが、そこで紹介されているパターンの多くはRustでもかなり一般的だと気づきました。ここでは、両方の言語で idiomatic なコードを横並びで比較していきます。
Rubyのコード例は元の記事から引用しています。
mapと高階関数
最初の例は、コンテナの要素をmapで反復処理する、ごく基本的なものです。
user_ids = users.map { |user| user.id }mapという考え方は、Rustでもごく標準的なものです。Rubyと比べると、ここでは少し明示的に書く必要があります。usersがUserオブジェクトのベクターなら、まずそこからイテレーターを作ります。
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}!"
endRust 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にはnilやNullという概念がないので、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を使っています。parseはResultを返しますが、filter_mapが要求するのはOptionです。Resultのok()を呼び出せば、ResultをOptionに変換できます。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の標準ライブラリには、スライス自体のメソッドとしてsampleやchooseは用意されていません。これは設計上の判断です。将来にわたって言語を発展させられるよう、言語のコアを小さく保っているのです。
だからといって、今すぐより使いやすい実装を用意できないわけではありません。たとえば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)
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では、1回の操作で複数の値を変数に代入できます。
def values
[1, 2, 3]
end
one, two, three = valuesRustでは、タプルを分解してタプルにすることはできますが、たとえばベクターをタプルに分解することはできません。したがって、これは動きます。
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()がResultをbooleanに変換すると書いていましたが、これは誤りでした。訂正してくれたisaacgに感謝します。↩
記事をランダムに読む