『The Rust Programming Language 2nd edition』の続き。
2章は推測ゲームの実装によるチュートリアル。
Setting Up a New Project
まずはCargoでプロジェクトを新規作成する。 1章でインストールだけしていきなり2章を読み始めても何とかなる安心設計。
$ cargo new guessing_game --bin Created binary (application) `guessing_game` project $ cd guessing_game $ cargo run Compiling guessing_game v0.1.0 (file:///guessing_game) Finished dev [unoptimized + debuginfo] target(s) in 2.80 secs Running `target\debug\guessing_game.exe` Hello, world!
cargo run
で必要に応じてビルドしてくれるので段階を踏みつつコマンドを叩けば継続的な開発ができる。
Processing a Guessr
最初はユーザーからの入力、入力値が期待通りであるかの確認を行う。
use std::io; fn main() { println!("Guess the number!"); println!("Please input your guess."); let mut guess = String::new(); io::stdin().read_line(&mut guess) .expect("Failed to read line."); println!("You guessed: {}", guess); }
cargo run
で実行できる。
$ cargo run Compiling guessing_game v0.1.0 (file:///guessing_game) Finished dev [unoptimized + debuginfo] target(s) in 1.30 secs Running `target\debug\guessing_game.exe` Guess the number! Please input your guess. 10 You guessed: 10
もっとプロンプト風にしたい...と思い、println!("Please input your guess.");
の部分を改行させないようにする。
print!
にすれば動きそうだ。
use std::io; fn main() { println!("Guess the number!"); print!("Please input your guess."); let mut guess = String::new(); io::stdin().read_line(&mut guess) .expect("Failed to read line."); println!("You guessed: {}", guess); }
cargo run
すると、
$ cargo run Compiling guessing_game v0.1.0 (file:///guessing_game) Finished dev [unoptimized + debuginfo] target(s) in 0.98 secs Running `target\debug\guessing_game.exe` Guess the number! 222 Please input your guess.You guessed: 222
おっと、期待した動作をしていない。
どうやら、改行\n
で出力をバッファからフラッシュしているようである。
そこで、無理やり(?)吐き出させる。
use std::io; use std::io::Write; // add for forced flush fn main() { println!("Guess the number!"); print!("Please input your guess: "); io::stdout().flush().unwrap(); // Forced flush!! let mut guess = String::new(); io::stdin().read_line(&mut guess) .expect("Failed to read line."); println!("You guessed: {}", guess); }
今度はどうなるかな。
$ cargo run Compiling guessing_game v0.1.0 (file:///guessing_game) Finished dev [unoptimized + debuginfo] target(s) in 1.17 secs Running `target\debug\guessing_game.exe` Guess the number! Please input your guess: 10 You guessed: 10
欲しかった形になった。
なお、io::stdout().flush()
はResult
型と呼ばれるものを返し、unwrap()
でその中身を取り出している処理を行っている。
詳しくはエラーハンドリングで行うらしい。
あくまでもここでは見栄えの問題なので深追いはしない。
閑話休題。このプログラムをひとつづつ調べる。
use std::io
で標準ライブラリからio
ライブラリを読み込む。上記の脇道だとさらにstd::io::Write
まで呼び出している。- デフォルトでRustは
prelude
と呼ばれるライブラリを読み込む。 - 1章を流し読みした人でも
fn
やprintln!
の説明が書かれているので安心。
Storing Values with Variables
let
で変数を指定する。デフォルトで、変数はイミュータブルとなる。- ミュータブルな変数にする場合は変数名の前に
mut
をつける。 let mut guess = String::new();
でミュータブルな変数guess
をString::new()
の返り値に束縛する。String
はUTF-8エンコードされた文字列。String::new
の::
はStringの関連函数(正確な日本語訳は不明)を呼び出す。io::stdin().read_line(&mut guess)
で標準入力から値を読み込んでguess
に渡す。guess
はミュータブルである。変数定義やread_line
の引数からmut
を外すとコンパイルエラーとなる。&
は変数の参照を意味する(C言語と一緒ですね)。Rustの売りの一つに参照を安全に扱うことが出来る点がある(という宣伝)。4章で詳しく取り扱う。参照もデフォルトはイミュータブルなのでミュータブルな参照は&mut guess
とする。
よく、C言語でscanf("%d",&a);
のような例が学習初期に出てきたときに「この&
はおまじないで...」とごまかす教材があるが、このチュートリアルはそれなりに分かっている人向けなので逃げずに参照と明言する。
Handling Potential Failure with the Result Type
read_line
の返り値はio::Result
型を返す。RustにはResult
型が標準ライブラリにたくさんある。Result
型は列挙型である。詳しくは6章。Result
はOk
とErr
の2つのバリアントを持つ、Ok
には正しく生成された値が、Err
にはなぜ、どのように失敗したのかの情報が含まれる。Result
の目的はエラーハンドリング情報をエンコードする(意味がうまく取れないが言わんとしていることはわかる)ためにある、io::Result
型のインスタンスはexpect
メソッドを持つ。io::Result
インスタンスがErr
の値を持っているならば、expcet
メソッドはプログラムをクラッシュさせ(!)expect
の引数に渡したメッセージを出力する。io::Result
がOk
の値を持っているならばexpect
はOk
が持つ値を返す。今回はユーザーが入力した値を返す。expect
を呼ばない場合はコンパイル時に警告が出る。
$ cargo build Compiling guessing_game v0.1.0 (file:///projects/guessing_game) warning: unused `std::result::Result` which must be used --> src/main.rs:10:5 | 10 | io::stdin().read_line(&mut guess); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: #[warn(unused_must_use)] on by default
Result
の値を使用していないとエラー処理をしていないということで警告を出す。
先ほどの閑話でプロンプトっぽいものを実現するためにバッファをフラッシュしている。
その際にio::stdout().flush().unwrap();
としているが、これもio::stdout().flush();
とすると警告が出る。
unwrap()
とすることでResult
型のインスタンスを処理(エラーの握りつぶし!)している。
Printing Values with println! Placeholders
println!("You guessed: {}", guess);
の{}
はプレースホルダ。他の言語にもよくある。
Testing the First Part
推測ゲーム実装の第一弾のテスト(標準入力)のテストを行う。
$ cargo run Compiling guessing_game v0.1.0 (file:///guessing_game) Finished dev [unoptimized + debuginfo] target(s) in 1.23 secs Running `target\debug\guessing_game.exe` Guess the number! Please input your guess: 1 You guessed: 1
確かに所望の動作を行っている。
Generating a Secret Number
推測ゲームなのでゲーム側が推測してほしい乱数を生成する必要がある。 当然、プレイのたびに推測する値は変化してほしい。
Rustには標準ライブラリに乱数ライブラリは存在しないが、Rustチームがrand
クレートを用意している。
クレートはRustのパッケージ。
Using a Crate to Get More Functionality
今まで作ってきたのはバイナリクレートであり、実行可能である。
rand
クレートはライブラリクレートで、別のプログラムから使われることを意図したものである。
外部ライブラリを使うときこそCargoが輝く(ビルドツールだからね!)。
rand
クレートを使うにはCargo.toml
に依存関係としてrand
を追加する。
[dependencies] rand = "0.3.14"
バージョン記述はセマンティックバージョニングに従う。
この状態でcargo build
を行う。
$ cargo build Updating registry `https://github.com/rust-lang/crates.io-index` Downloading rand v0.3.22 Downloading libc v0.2.39 Downloading rand v0.4.2 Downloading winapi v0.3.4 Compiling winapi v0.3.4 Compiling libc v0.2.39 Compiling rand v0.4.2 Compiling rand v0.3.22 Compiling guessing_game v0.1.0 (file:///guessing_game) Finished dev [unoptimized + debuginfo] target(s) in 11.78 secs
外部ライブラリはCrates.ioから取得する。 最近の言語(Perl以降?)はこの手のレポジトリが必ずある気がする。 TeXだってCTANがあるのだし。
ドキュメントにある出力結果とは異なり、環境がWindowsであったのでWindows向けと思われるライブラリクレートwinapi
も一緒にやってきた。
The Cargo.lock File Ensures Reproducible Builds
Cargo.lock
ファイルでバージョンの固定ができる。
cargo.toml
ではセマンティックバージョニングなので以上やら以下やらが入るが、一度実行すれば明示的にアップグレードするまではCargo.lock
ファイルの記述に従う。
このあたり、Pythonのrequirements.txt
との違いにまだ体が慣れていない。
pip freeze > requirements.txt
にあたる操作であろう。
Updating a Crate to Get a New Version
Cargo.lock
を無視して指定したバージョンを満たす最新版を探してアップデートできたらCargo.lock
に書き込むcargo update
コマンドがある。
しかし、私の環境では何も起きなかった...。
Generating a Random Number
やっと乱数を使う。
extern crate rand; use std::io; use std::io::Write; use rand::Rng; fn main() { println!("Guess the number!"); let secret_number = rand::thread_rng().gen_range(1, 101); println!("The secret number is {}", secret_number); print!("Please input your guess: "); io::stdout().flush().unwrap();; let mut guess = String::new(); io::stdin().read_line(&mut guess) .expect("Failed to read line."); println!("You guessed: {}", guess); }
推測する数字は不変であるべきなのでmut
はつけていない。
外部のライブラリクレートを使うにはextern crate rand;
のようにする。
これでrand
クレートがrand::
で使える。
thread_rng()
は乱数ジェネレータの1つ。
gen_range
は引数に渡した範囲の乱数を生成するメソッド(関連函数か?)。
rand :: Rng
ステートメントなるものがスコープに必要らしくuse rand::Rng;
を追加している。
追加し忘れると、以下のような憂き目にあう。
error[E0599]: no method named `gen_range` found for type `rand::ThreadRng` in the current scope --> src\main.rs:10:44 | 10 | let secret_number = rand::thread_rng().gen_range(1, 101); | ^^^^^^^^^ | = help: items from traits can only be used if the trait is in scope help: the following trait is implemented but not in scope, perhaps add a `use` for it: | 3 | use rand::Rng; | error: aborting due to previous error error: Could not compile `guessing_game`.
とはいえ、きちんとエラーメッセージに案内がでるので(ドキュメントを読めという話であるが)安心である。
cargo doc --open
でドキュメントが読めるのでこれは読めということである。
コンパイルできるように修正して実行すると、確かに乱数が生成されている。
$ cargo run Compiling guessing_game v0.1.0 (file:///guessing_game) Finished dev [unoptimized + debuginfo] target(s) in 1.75 secs Running `target\debug\guessing_game.exe` Guess the number! The secret number is 66 Please input your guess: 10 You guessed: 10 $ cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs Running `target\debug\guessing_game.exe` Guess the number! The secret number is 38 Please input your guess: 1 You guessed: 1
Comparing the Guess to the Secret Number
乱数と入力値を比較する。
extern crate rand; use std::cmp::Ordering; use std::io; use std::io::Write; use rand::Rng; fn main() { println!("Guess the number!"); let secret_number = rand::thread_rng().gen_range(1, 101); println!("The secret number is {}", secret_number); print!("Please input your guess: "); io::stdout().flush().unwrap();; let mut guess = String::new(); io::stdin().read_line(&mut guess) .expect("Failed to read line."); println!("You guessed: {}", guess); match guess.cmp(&secret_number){ Ordering::Less => println!("Too small!"), Ordering::Greater => println!("Too big!"), Ordering::Equal => println!("You win!"), } }
std::cmp::Ordering
は標準ライブラリにあり、Result
型と同様に列挙型であるが、Less
、Greater
、Equal
を持つ。
guess.cmp
のcmp
メソッドでsecret_number
と比較を行う。
match
はパターンマッチングか。昔、Haskellでもパターンマッチングを見て強力な云々という解説を読んだ覚えがあるが、Rustでもやはり重要なのだろうか。
実際に動かすと、
$ cargo build Compiling guessing_game v0.1.0 (file:///guessing_game) error[E0308]: mismatched types --> src\main.rs:25:21 | 25 | match guess.cmp(&secret_number){ | ^^^^^^^^^^^^^^ expected struct `std::string::String`, found integral variable | = note: expected type `&std::string::String` found type `&{integer}` error: aborting due to previous error error: Could not compile `guessing_game`.
おっと、文字列型と数値型(ここでは符号つき32bit整数)を比較しているようである(ドキュメント通りの誘導に乗っかっている)。 PHPやPerlでは「いい感じに」解釈して比較してくれた気がするが、どちらが幸せだろうか。 僕は比較できないほうが幸せだと思う。
適切な比較をするために、入力値を実数(?)に変換して比較を行う。
extern crate rand; use std::cmp::Ordering; use std::io; use std::io::Write; use rand::Rng; fn main() { println!("Guess the number!"); let secret_number = rand::thread_rng().gen_range(1, 101); println!("The secret number is {}", secret_number); print!("Please input your guess: "); io::stdout().flush().unwrap();; let mut guess = String::new(); io::stdin().read_line(&mut guess) .expect("Failed to read line."); let guess: u32 = guess.trim().parse() .expect("Please type a number!"); println!("You guessed: {}", guess); match guess.cmp(&secret_number){ Ordering::Less => println!("Too small!"), Ordering::Greater => println!("Too big!"), Ordering::Equal => println!("You win!"), } }
変数guess
を再利用できる。
このような操作をシャドーイングと呼び、詳しくは3章で取り扱う。
型に厳密にありながらこの機能は便利である(が、変なミスにもつながりそう)。
標準入力の値には改行が含まれるので、trim()
で改行を取り除く。trim()
はPerlっぽい印象をうける。
parse()
で文字列を何かしらの数値に変換する。
「何かしら」だとどの数値型であるのかがわからないのでlet guess: u32
のように注釈をつけてRustに教える。
ここでは符号なし32bit整数に変換している。
また、文字を数値として変換できない場合も多々あるので、きちんとResult
型の処理を行う。
きちんと変換できたので、実行する。
$ cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs Running `target\debug\guessing_game.exe` Guess the number! The secret number is 80 Please input your guess: 77 You guessed: 77 Too small!
うまくいった。
なお、標準入力に負数を入れると破綻する。
$ cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs Running `target\debug\guessing_game.exe` Guess the number! The secret number is 5 Please input your guess: -1 thread 'main' panicked at 'Please type a number!: ParseIntError { kind: InvalidDigit }', src\libcore\result.rs:916:5 note: Run with `RUST_BACKTRACE=1` for a backtrace. error: process didn't exit successfully: `target\debug\guessing_game.exe` (exit code: 101)
Allowing Multiple Guesses with Looping
現状だと1回しかできないので、繰り返し実行させる。
extern crate rand; use std::cmp::Ordering; use std::io; use std::io::Write; use rand::Rng; fn main() { println!("Guess the number!"); let secret_number = rand::thread_rng().gen_range(1, 101); println!("The secret number is {}", secret_number); loop { print!("Please input your guess: "); io::stdout().flush().unwrap();; let mut guess = String::new(); io::stdin().read_line(&mut guess) .expect("Failed to read line."); let guess: u32 = guess.trim().parse() .expect("Please type a number!"); println!("You guessed: {}", guess); match guess.cmp(&secret_number){ Ordering::Less => println!("Too small!"), Ordering::Greater => println!("Too big!"), Ordering::Equal => println!("You win!"), } } }
ぱっと見無限ループなので怖いが実行する。
$ cargo build Compiling guessing_game v0.1.0 (file:///guessing_game) Finished dev [unoptimized + debuginfo] target(s) in 1.67 secs $ cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs Running `target\debug\guessing_game.exe` Guess the number! The secret number is 88 Please input your guess: 3 You guessed: 3 Too small! Please input your guess: 89 You guessed: 89 Too big! Please input your guess: 88 You guessed: 88 You win! Please input your guess: quit thread 'main' panicked at 'Please type a number!: ParseIntError { kind: InvalidDigit }', src\libcore\result.rs:916:5 note: Run with `RUST_BACKTRACE=1` for a backtrace. error: process didn't exit successfully: `target\debug\guessing_game.exe` (exit code: 101)
やはり無限ループだった。
Quitting After a Correct Guess
正解を入力したら停止させたいのは当然そう思うことである。 正解したらループを抜けるようにする。
extern crate rand; use std::cmp::Ordering; use std::io; use std::io::Write; use rand::Rng; fn main() { println!("Guess the number!"); let secret_number = rand::thread_rng().gen_range(1, 101); println!("The secret number is {}", secret_number); loop { print!("Please input your guess: "); io::stdout().flush().unwrap();; let mut guess = String::new(); io::stdin().read_line(&mut guess) .expect("Failed to read line."); let guess: u32 = guess.trim().parse() .expect("Please type a number!"); 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; } } } }
実行する。
$ cargo build Compiling guessing_game v0.1.0 (file:///guessing_game) Finished dev [unoptimized + debuginfo] target(s) in 1.43 secs $ cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs Running `target\debug\guessing_game.exe` Guess the number! The secret number is 7 Please input your guess: 7 You guessed: 7 You win!
Handling Invalid Input
まちがった入力してもゲームを続けることができるようにしよう。
extern crate rand; use std::cmp::Ordering; use std::io; use std::io::Write; use rand::Rng; fn main() { println!("Guess the number!"); let secret_number = rand::thread_rng().gen_range(1, 101); println!("The secret number is {}", secret_number); loop { print!("Please input your guess: "); io::stdout().flush().unwrap();; 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; } } } }
今までexpect
でクラッシュさせていた部分をmatch
でエラーハンドリングを行っている。
let guess: u32 = match guess.trim().parse() { Ok(num) => num, Err(_) => continue, };
見た目通りの動きをしている。
最後に、デバッグ用のprint!
マクロを削除して推測ゲームの実装が完了する。
extern crate rand; use std::cmp::Ordering; use std::io; use std::io::Write; use rand::Rng; fn main() { println!("Guess the number!"); let secret_number = rand::thread_rng().gen_range(1, 101); loop { print!("Please input your guess: "); io::stdout().flush().unwrap();; 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; } } } }
Summary
Rustのいくつかの概念(let
, match
、メソッド、関連函数、外部クレート)を実践的な方法で学んだ(完全に理解できたとは言っていない)。
3章ではよくある概念の話、4章ではRust特有のオーナ0シップの話、5章は構造体とメソッド、6章では列挙型について。
3章は他の言語の経験があればおそらく読めると思うが、4章のオーナーシップが山場になりそうである。