何かを書き留める何か

数学や読んだ本について書く何かです。最近は社会人として生き残りの術を学ぶ日々です。

『The Rust Programming Language 2nd edition』読書記録 その2

『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章を流し読みした人でもfnprintln!の説明が書かれているので安心。

Storing Values with Variables

  • let で変数を指定する。デフォルトで、変数はイミュータブルとなる。
  • ミュータブルな変数にする場合は変数名の前にmutをつける。
  • let mut guess = String::new(); でミュータブルな変数guessString::new()の返り値に束縛する。
  • StringUTF-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章。
  • ResultOkErrの2つのバリアントを持つ、Okには正しく生成された値が、Errにはなぜ、どのように失敗したのかの情報が含まれる。
  • Resultの目的はエラーハンドリング情報をエンコードする(意味がうまく取れないが言わんとしていることはわかる)ためにある、io::Result型のインスタンスexpectメソッドを持つ。
  • io::ResultインスタンスErrの値を持っているならば、expcetメソッドはプログラムをクラッシュさせ(!)expectの引数に渡したメッセージを出力する。
  • io::ResultOkの値を持っているならばexpectOkが持つ値を返す。今回はユーザーが入力した値を返す。
  • 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

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ファイルの記述に従う。 このあたり、Pythonrequirements.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型と同様に列挙型であるが、LessGreaterEqualを持つ。 guess.cmpcmpメソッドで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整数)を比較しているようである(ドキュメント通りの誘導に乗っかっている)。 PHPPerlでは「いい感じに」解釈して比較してくれた気がするが、どちらが幸せだろうか。 僕は比較できないほうが幸せだと思う。

適切な比較をするために、入力値を実数(?)に変換して比較を行う。

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章のオーナーシップが山場になりそうである。