Rust入門 ~その2 Programming a Guessing Gameを理解する~
前回の記事
初めに
前回はローカル環境を作ってRocketを使ったAPIのクイックスタートを動かしました。
今回は早速APIを作ってみようかなと思ったのですが、クイックスタートのサンプルコードを見てもなんとなくわかるようなわからないようなという感じだったので、まずはRustの構文やビルドツールについて理解するところから始めたいと思います。
ということで、今回はThe bookのProgramming a Guessing Gameの内容を理解していきたいと思います。
ソースコード全文(The bookの内容そのまま)
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1, 101);
loop {
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
println!("You guessed: {}", guess);
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
理解したこと
Programming a Guessing Gameを読んで理解したことのうち、印象的だったものをまとめます。
cargo
cargoがrustのパッケージマネージャのようです。
javaでいうところのgradleと理解しました。
Programming a Guessing Gameで登場したコマンドは以下の通りです。
コマンド | 説明 |
---|---|
cargo new <プロジェクト名> | 新規プロジェクトを作成する |
cargo run | プロジェクトのアプリを実行する |
cargo build | プロジェクトをビルドする |
cargo update | 依存ライブラリ(crate)をパッチバージョンアップする |
cargo doc --open | 依存ライブラリのドキュメントを取得する |
cargo updateがパッチバージョンしか上げないというのがいいですね。
使用しているcrateがセマンティックバージョニングに準拠していれば、updateを行っても自分のアプリケーションの振る舞いは変わらないということなので安心して実行できそうです。
ただし、本当に振る舞いが変わらないかは使用しているcrateに依存するのでテストは必要だと思います。
依存関係をrand = "0.5.5"
のように書くと、cargoは0.5.5と互換性のある新しいバージョンを取得してくるようです。
ビルドのたびに依存しているcrateのバージョンが変わっていってしまうと不都合が生じる可能性があるため、一度依存関係の解決をすると.lockファイルが作成され、次回以降は.lockファイルに保存されたバージョンを取得するようです。
npmやyarnと似ていますね。
すでに登場していますが、Rustではパッケージングされた単位のことをcrateと呼ぶようです。
cargoが依存関係を解決する際は、Cargo.tomlで定義された内容をもとにCrates.ioからcrateを取得してくるようですね。
moduleとuse
moduleがRustにおける名前空間のようです。
異なるmoduleで定義されたものを使用する際にはmodule含め指定することで利用することができるとのことです。
ただし、毎回ルートのmoduleから記載していると冗長になるのでuse
を使用することで省略可能です。
useを使用しない場合の例
// 略
match guess.cmp(&secret_number) {
std::cmp::Ordering::Less => println!("Too small!"),
std::cmp::Ordering::Greater => println!("Too big!"),
std::cmp::Ordering::Equal => {
println!("You win!");
break;
},
}
// 略
useを使用する場合の例
use std::cmp::Ordering;
// 略
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
},
}
// 略
明示的にuse
で宣言しなくても使用できるmoduleのことをpreludeというそうです。
prelude(日本語で前奏)とは、なんだかおしゃれな命名ですね。
意味も通じますし、個人的には好きな名称です。
マクロ
マクロについてはまだあまり理解できていないので、厳密には理解したものではありませんが...
println!
のように!
が末尾につくものはマクロというそうです。
関数との違いについてはいまいちわかりませんでしたが、一旦は関数のシンタックスシュガーみたいなものととらえておきます。
(必要性を感じたらちゃんと違いを調べます)
let
Rustではlet
で宣言するとimmutableとして扱われるのですね。
デフォルトがimmutableなのは安心感があります。
ただし、別の言語から来た人がいきなりコードを見ると戸惑うかもしれません。
mutableな変数として宣言するためにはlet mute <変数名>
で宣言するようです。
また、Rustは型推論を備えているようです。
以下のコードからわかるように変数宣言時に型を明示していません。
しかし、vscodeでsecret_number
にマウスオーバーするとちゃんとu32と表示されます。
let secret_number = rand::thread_rng().gen_range(1, 101);
一方で以下のコードでは型を明示的に宣言しています。
これはparse()が複数の型をサポートしていることで、明示的な宣言がないとどの型に変換したらよいか判断できないためのようです。
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
Result
Rustでは関数の戻り値をResultというenumでラップするようですね。
私が普段業務で使っているJavaやC#はエラー時には例外を発するため、考え方が違いそうです。
Rustにおけるエラーの伝え方はgoに似ていそうですね。
(Rustもgoも初心者なので違ったらご指摘ください)
Resultを受け取ったプログラムがErrをハンドリングしていない場合は、warningが出るみたいなのでコンパイル時に考慮漏れに気付けるのは良いですね。
match
Rustはパターンマッチを実装しているようですね。
数値比較をパターンマッチで行っているのは、私にとって新鮮でした。
以下のコードはguessとsecret_numberを比較した結果に応じた分岐を記述しています。
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
if / else ifでつなぐより簡潔で良いですね。
次回
まだRocketを使ったAPI開発に進むには早い気がするので、もっとThe bookを読み進めたいと思います。
Discussion
細かいですがtypo発見したので報告しておきます...!
mute -> mut