モッピー!ポイ活応援ポイントサイト
未分類

Rust 2 数当てゲーム

https://doc.rust-jp.rs/book-ja/ch02-00-guessing-game-tutorial.html

数当てゲームをプログラムする

実物のプロジェクトに一緒に取り組むことで、Rustの世界へ飛び込みましょう! この章では、実際のプログラム内で使用しながらいくつかの一般的なRustの概念に触れます。 letmatch、メソッド、関連関数、外部クレートの使用などについて学ぶでしょう! 後ほどの章でこれらの概念について深く知ることになります。この章では、基礎部分だけにしましょう。

古典的な初心者向けのプログラミング問題を実装してみましょう: 数当てゲームです。 これは以下のように動作します: プログラムは1から100までの乱数整数を生成します。 そしてプレーヤーに予想を入力するよう促します。予想を入力したら、プログラムは、 その予想が小さすぎたか大きすぎたかを出力します。予想が当たっていれば、ゲームは祝福メッセージを表示し、 終了します。

新規プロジェクトの立ち上げ

新規プロジェクトを立ち上げるには、第1章で作成したprojectsディレクトリに行き、 Cargoを使って新規プロジェクトを作成します。以下のように:

cargo new guessing_game --bin
cd guessing_game

最初のコマンドcargo newは、プロジェクト名を第1引数に取ります(guessing_gameですね)。 --binというフラグは、Cargoにバイナリ生成プロジェクトを作成させます。第1章のものと似ていますね。 2番目のコマンドで新規プロジェクトのディレクトリに移動します。

生成されたCargo.tomlファイルを見てください:

ファイル名: Cargo.toml

[package]
name = "guessing_game"
version = "0.1.0"
authors = ["名前 <you@example.com>"]

[dependencies]

もし、Cargoがあなたの環境から取得した作者情報が間違っていたら、 ファイルを編集して保存し直してください。

第1章でも見かけたように、cargo newコマンドは、”Hello, world!”プログラムを生成してくれます。 src/main.rsファイルをチェックしてみましょう:

ファイル名: src/main.rs

fn main() {
    println!("Hello, world!");
}

さて、この”Hello, world!”プログラムをコンパイルし、cargo runコマンドを使用して、 以前と同じように動かしてみましょう:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 1.50 secs
     Running `target/debug/guessing_game`
Hello, world!

runコマンドは、プロジェクトに迅速に段階を踏んで取り掛かる必要がある場合に有用であり、 次のステップに進む前に各段階を急速にテストして、このゲームではそれを行います。

再度src/main.rsファイルを開きましょう。ここにすべてのコードを書いていきます。

予想を処理する

数当てプログラムの最初の部分は、ユーザに入力を求め、その入力を処理し、予期した形式になっていることを確認します。 手始めに、プレーヤーが予想を入力できるようにしましょう。 リスト2-1のコードをsrc/main.rsに入力してください。

ファイル名: src/main.rs

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);     // 次のように予想しました: {}
}

リスト2-1: ユーザに予想を入力してもらい、それを出力するコード

注釈: The programming language Rust第1版の翻訳者によると、 ソースコードのコメント中以外に日本語文字があるとコンパイルに失敗することがあるそうなので、文字列の英語は、コメントに和訳を載せます。 また、重複する内容の場合には、最初の1回だけ掲載するようにします。

このコードには、たくさんの情報が詰め込まれていますね。なので、行ごとに見ていきましょう。 ユーザ入力を受け付け、結果を出力するためには、io(入/出力)ライブラリをスコープに導入する必要があります。 ioライブラリは、標準ライブラリ(stdとして知られています)に存在します:

stdとは標準ライブラリのこと

use std::io;

デフォルトでは、preludeに存在するいくつかの型のみ使えます。 もし、使用したい型がpreludeにない場合は、use文で明示的にその型をスコープに導入する必要があります。 std::ioライブラリを使用することで、ユーザ入力を受け付ける能力などの実用的な機能の多くを使用することができます。

第1章で見た通り、main関数がプログラムへのエントリーポイント(脚注: スタート地点)になります:

fn main() {

fn構文が関数を新しく宣言し、かっこの()は引数がないことを示し、波括弧の{が関数本体のスタート地点になります。

また、第1章で学んだように、println!は、文字列を画面に表示するマクロになります:

println!("Guess the number!");

println!("Please input your guess.");

このコードは、このゲームが何かを出力し、ユーザに入力を求めています。

値を変数に保持する

次に、ユーザ入力を保持する場所を作りましょう。こんな感じに:

let mut guess = String::new();

さあ、プログラムが面白くなってきましたね。このたった1行でいろんなことが起きています。 これがlet文であることに注目してください。これを使用して変数を生成しています。 こちらは、別の例です:

letで変数を生成する。Rustは標準で、変数は不変。変数なのに不変。。。mutをつけて定義すると可変になる。
let foo = bar;

この行では、fooという名前の新しい変数を作成し、barの値に束縛しています。 Rustでは、変数は標準で不変(immutable)です。この概念について詳しくは、 第3章の「変数と可変性」節で議論します。以下の例には、 変数名の前にmutをつけて変数を可変にする方法が示されています:

let foo = 5; // immutable
let mut bar = 5; // mutable

注釈: //という記法は、行末まで続くコメントを記述します。 コンパイラは、コメントを一切無視し、これについても第3章で詳しく議論します。

数当てゲームのプログラムに戻りましょう。さあ、let mut guessguessという名前の可変変数を導入するとわかりましたね。 イコール記号(=)の反対側には、変数guessが束縛される値があります。この値は、 String::new関数の呼び出し結果であり、この関数は、String型のオブジェクトを返します。 String型は、標準ライブラリによって提供される文字列型で、 サイズ可変、UTF-8エンコードされたテキスト破片になります。

::new行にある::という記法は、newString型の関連関数であることを表しています。 関連関数とは、String型の特定のオブジェクトよりも型(この場合はString)に対して 実装された関数のことであり、静的(スタティック)メソッドと呼ばれる言語もあります。

関連関数とは、特定のオブジェクトではなく、ストリング型やナンバー型などの、型、に対して実装された関数。静的メソッドとも言われる。

(::は、TSでの.のようなものか。string.new()のイメージかな。)

このnew関数は、新しく空の文字列を生成します。new関数は、いろんな型に見られます。 なぜなら、何らかの新規値を生成する関数にとってありふれた名前だからです。

ストリング型のnew関数で新しい文字列を作成する。

まとめると、let mut guess = String::new();という行は、現在、新たに空のStringオブジェクトに束縛されている 可変変数を作っているわけです。ふう!

プログラムの1行目で、use std::ioとして、標準ライブラリから入/出力機能を取り込んだことを思い出してください。 今度は、ioモジュールのstdin関数を呼び出しましょう:

io::stdin().read_line(&mut guess)
    .expect("Failed to read line");

仮に、プログラムの冒頭でuse std::ioとしていなければ、この関数呼び出しは、std::io::stdinと記述していたでしょう。 このstdin関数は、 std::io::Stdinオブジェクトを返し、この型は、 ターミナルの標準入力へのハンドルを表す型になります。

その次のコード片、.read_line(&mut guess)は、標準入力ハンドルのread_line メソッドを呼び出して、ユーザから入力を受け付けます。また、read_lineメソッドに対して、&mut guessという引数を一つ渡していますね。

read_lineメソッドの仕事は、ユーザが標準入力したものすべてを取り出し、文字列に格納することなので、 格納する文字列を引数として取ります。この文字列引数は、可変である必要があります。 メソッドがユーザ入力を追記して、文字列の中身を変えられるようにということですね。

readlineメソッドでユーザーに入力してもらい、先程生成したguessに代入する。

&によって、同じデータにアクセスして、変数を変更できる。

&という記号は、この引数が参照であることを表し、これのおかげで、データを複数回メモリにコピーせずとも、 コードの複数箇所で同じデータにアクセスできるようになるわけです。参照は複雑な機能であり、 とても安全かつ簡単に参照を使うことができることは、Rustの主要な利点の一つでもあります。 そのような詳細を知らなくても、このプログラムを完成させることはできます。 現時点では、変数のように、参照も標準で不変であることを知っておけばいいでしょう。 故に、&guessと書くのではなく、&mut guessと書いて、可変にする必要があるのです。 (第4章で参照についてより詳細に説明します)

参照も変数のように、標準では不変なので、いちいち&mutとする必要がある。

Result型で失敗の可能性を扱う

まだ、この行は終わりではありませんよ。ここまでに議論したのはテキストでは1行ですが、コードとしての論理行としては、 まだ所詮最初の部分でしかないのです。2番目の部分はこのメソッドです:

.expect("Failed to read line");

.foo()という記法で、メソッドを呼び出す時、改行と空白で長い行を分割するのがしばしば賢明です。 今回の場合、こう書くこともできますよね:

io::stdin().read_line(&mut guess).expect("Failed to read line");

しかし、長い行は読みづらいものです。なので、分割しましょう: 2回のメソッド呼び出しに、2行です。 さて、この行が何をしているのかについて議論しましょうか。

以前にも述べたように、read_lineメソッドは、渡された文字列にユーザが入力したものを入れ込むだけでなく、 値も返します(今回はio::Resultです)。 RustにはResultと名のついた型が、 標準ライブラリにたくさんあります: 汎用のResultの他、 io::Resultなどのサブモジュール用に特化したものまで。

このResult型は、列挙型であり、普通、enum(イーナム)と呼ばれます。 列挙型とは、固定された種類の値を持つ型のことであり、それらの値は、enumの列挙子(variant)と呼ばれます。 enumについては、第6章で詳しく解説します。

Result型に関しては、列挙子はOkErrです。Ok列挙子は、処理が成功したことを表し、 中に生成された値を保持します。Err列挙子は、処理が失敗したことを意味し、Errは、処理が失敗した過程や、 理由などの情報を保有します。

これらResult型の目的は、エラー処理の情報をコード化することです。Result型の値も、他の型同様、 メソッドが定義されています。io::Resultオブジェクトには、呼び出し可能なexpectメソッドがあります。 このio::ResultオブジェクトがErr値の場合、expectメソッドはプログラムをクラッシュさせ、 引数として渡されたメッセージを表示します。read_lineメソッドがErrを返したら、 恐らく根底にあるOSによるエラーに起因するのでしょう。 このio::ResultオブジェクトがOk値の場合、expectメソッドは、Ok列挙子が保持する 返り値を取り出して、ただその値を返すので、これを使用することができるでしょう。 今回の場合、その返り値とは、ユーザが標準入力に入力したデータのバイト数になります。

expectを使うと、resultがエラーだったときに、引数を表示できる。

もし、expectメソッドを呼び出さなかったら、コンパイルは通るものの、警告が出るでしょう:

$ cargo build
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
warning: unused `std::result::Result` which must be used
(警告: 使用されなければならない`std::result::Result`が使用されていません)
  --> src/main.rs:10:5
   |
10 |     io::stdin().read_line(&mut guess);
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: #[warn(unused_must_use)] on by default

コンパイラは、私たちがread_lineメソッドから返ってきたResult値を使用していないと警告してきており、 これは、プログラムがエラーの可能性に対処していないことを示します。

警告を抑制する正しい手段は、実際にエラー対処コードを書くことですが、今は、 問題が起きた時にプログラムをクラッシュさせたいので、expectを使用できるわけです。 エラーから復旧する方法については、第9章で学ぶでしょう。

println!マクロのプレースホルダーで値を出力する

閉じ波かっこを除けば、ここまでに追加されたコードのうち議論すべきものは、残り1行であり、それは以下の通りです:

println!("You guessed: {}", guess);

この行は、ユーザ入力を保存した文字列の中身を出力します。1組の波括弧の{}は、プレースホルダーの役目を果たします: {}は値を所定の場所に保持する小さなカニのはさみと考えてください。波括弧を使って一つ以上の値を出力できます: 最初の波括弧の組は、フォーマット文字列の後に列挙された最初の値に対応し、 2組目は、2つ目の値、とそんな感じで続いていきます。1回のprintln!の呼び出しで複数の値を出力するコードは、 以下のような感じになります:


let x = 5;
let y = 10;

println!("x = {} and y = {}", x, y);

このコードは、x = 5 and y = 10と出力するでしょう.

最初の部分をテストする

数当てゲームの最初の部分をテストしてみましょう。cargo runでプログラムを走らせてください:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 2.53 secs
     Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
6
You guessed: 6

ここまでで、ゲームの最初の部分は完成になります: キーボードからの入力を受け付け、出力できています。

秘密の数字を生成する

次に、ユーザが数当てに挑戦する秘密の数字を生成する必要があります。毎回この秘密の数字は、変わるべきです。 ゲームが何回も楽しめるようにですね。ゲームが難しくなりすぎないように、1から100までの乱数を使用しましょう。 Rustの標準ライブラリには、乱数機能はまだ含まれていません。ですが、実は、 Rustの開発チームがrandクレートを用意してくれています。

クレートを使用して機能を追加する

クレートはRustコードのパッケージであることを思い出してください。私たちがここまで作ってきたプロジェクトは、 バイナリクレートであり、これは実行可能形式になります。randクレートはライブラリクレートであり、 他のプログラムで使用するためのコードが含まれています。

クレートには2つある。実行可能なバイナリクレートと、ライブラリクレート。外部のライブラリクレートを使うときには、cargoが活きる

外部クレートを使用する部分は、Cargoがとても輝くところです。randを使ったコードを書ける前に、 Cargo.tomlファイルを編集して、randクレートを依存ファイルとして取り込む必要があります。 今このファイルを開いて、以下の行をCargoが自動生成した[dependencies]セクションヘッダの一番下に追記しましょう:

ファイル名: Cargo.toml

[dependencies]

rand = "0.3.14"

  外部クレートを使いたいときは、Cargo.tomlを編集して、依存ファイルとして読み込むようにする。

Cargo.tomlファイルにおいて、ヘッダに続くものは全て、他のセクションが始まるまで続くセクションの一部になります。 [dependecies]セクションは、プロジェクトが依存する外部クレートと必要とするバージョンを記述するところです。 ここでは、randクレートで、セマンティックバージョン指定子には0.3.14を指定します。Cargoは、 バージョンナンバー記述の標準規格であるセマンティックバージョニング (時にSemVerと呼ばれる)を理解します。 0.3.14という数字は、実際には^0.3.14の省略記法で、これは、「バージョン0.3.14と互換性のある公開APIを持つ任意のバージョン」を意味します。

さて、コードは一切変えずに、リスト2-2のようにプロジェクトをビルドしましょう。

ビルドすると自動でパッケージをダウンロードしてくれる

Downloaded rand v0.3.23
Downloaded 1 crate (11.3 KB) in 0.74s
Compiling libc v0.2.112
Compiling rand v0.4.6
Compiling rand v0.3.23

$ cargo build
    Updating registry `https://github.com/rust-lang/crates.io-index` (レジストリを更新しています)
 Downloading rand v0.3.14                                            (rand v0.3.14をダウンロードしています)
 Downloading libc v0.2.14                                            (libc v0.2.14をダウンロードしています)
   Compiling libc v0.2.14                                            (libc v0.2.14をコンパイルしています)
   Compiling rand v0.3.14                                            (rand v0.3.14をコンパイルしています)
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)   (guessing_game v0.1.0をコンパイルしています)
    Finished dev [unoptimized + debuginfo] target(s) in 2.53 secs

リスト2-2: randクレートを依存として追加した後のcargo buildコマンドの出力

もしかしたら、バージョンナンバーは違うかもしれません(でも、互換性はあります、SemVerのおかげでね!)。 そして、行の出力順序も違うかもしれません。

今や、外部依存を持つようになったので、Cargoはレジストリ(registry、登録所)から最新バージョンを拾ってきます。 レジストリとは、Crates.ioのデータのコピーです。Crates.ioとは、Rustのエコシステムにいる人間が、 他の人が使えるように自分のオープンソースのRustプロジェクトを投稿する場所です。

レジストリの更新後、Cargoは[dependencies]セクションをチェックし、まだ取得していないクレートを全部ダウンロードします。 今回の場合、randしか依存ファイルには列挙していませんが、Cargoはlibcのコピーも拾ってきます。 randクレートがlibcに依存しているからですね。クレートのダウンロード完了後、コンパイラは依存ファイルをコンパイルし、 依存が利用可能な状態でプロジェクトをコンパイルします。

何も変更せず即座にcargo buildコマンドを走らせたら、Finished行を除いて何も出力されないでしょう。 Cargoは、既に全ての依存をダウンロードしてコンパイル済みであることも、 あなたがCargo.tomlファイルを弄ってないことも知っているからです。さらに、Cargoはプログラマがコードを変更していないことも検知するので、 再度コンパイルすることもありません。することがないので、ただ単に終了します。

src/main.rsファイルを開き、些細な変更をし、保存して再度ビルドを行えば、2行だけ出力があるでしょう:

$ cargo build
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 2.53 secs

これらの行は、Cargoがsrc/main.rsファイルへの取るに足らない変更に対して、ビルドを更新していることを示しています。 依存は変更していないので、Cargoは、既にダウンロードしてコンパイルまで済ませてある依存を使用できると検知します。 自分で書いたコードのみ再ビルドをかけるわけです。

Cargo.lockファイルで再現可能なビルドを保証する

Cargoは、プログラマが自分のコードを更新するたびに同じ生成物を再構成することを保証してくれるメカニズムを備えています: Cargoは、プログラマが示唆するまで、指定したバージョンの依存のみを使用します。 例として、randクレートの次週のバージョン0.3.15が登場し、重要なバグ修正がなされているけれども、 自分のコードを破壊してしまう互換性破壊があった場合はどうなるでしょう?

この問題に対する回答は、Cargo.lockファイルであり、このファイルは、初めてcargo buildコマンドを 走らせた時に生成され、現在guessing_gameディレクトリに存在しています。プロジェクトを初めてビルドする際に、 Cargoは判断基準(criteria)に合致するよう全ての依存のバージョンを計算し、Cargo.lockファイルに記述します。 次にプロジェクトをビルドする際には、CargoはCargo.lockファイルが存在することを確かめ、 再度バージョンの計算の作業を行うのではなく、そこに指定されているバージョンを使用します。 このことにより、自動的に再現可能なビルドを構成できるのです。つまり、明示的にアップグレードしない限り、 プロジェクトが使用するバージョンは0.3.14に保たれるのです。Cargo.lockファイルのおかげでね。

Cargo.lockによって、バージョンがアップデートされてもユーザーが明示的にバージョンを変えなければ前のバージョンを使ってくれる。バージョンのアップグレードによるバグに悩まなくていい!

クレートを更新して新バージョンを取得する

クレートを本当にアップグレードする必要が出てきたら、Cargoは別のコマンド(update)を提供します。 これは、Cargo.lockファイルを無視して、Cargo.tomlファイル内の全ての指定に合致する最新バージョンを計算します。 それがうまくいったら、CargoはそれらのバージョンをCargo.lockファイルに記述します。

しかし標準でCargoは、0.3.0より大きく、0.4.0未満のバージョンのみを検索します。 randクレートの新バージョンが2つリリースされていたら(0.3.150.4.0だとします)、 cargo updateコマンドを走らせた時に以下のようなメッセージを目の当たりにするでしょう:

$ cargo update
    Updating registry `https://github.com/rust-lang/crates.io-index`
    (レジストリ`https://github.com/rust-lang/crates-io-index`を更新しています)
    Updating rand v0.3.14 -> v0.3.15
    (randクレートをv0.3.14 -> v0.3.15に更新しています)

この時点で、Cargo.lockファイルに書かれている現在使用しているrandクレートのバージョンが、 0.3.15になっていることにも気付くでしょう。

randのバージョン0.4.0または、0.4.xシリーズのどれかを使用したかったら、 代わりにCargo.tomlファイルを以下のように更新しなければならないでしょう:

[dependencies]

rand = "0.4.0"

次回、cargo buildコマンドを走らせたら、Cargoは利用可能なクレートのレジストリを更新し、 randクレートの必要条件を指定した新しいバージョンに従って再評価します。

まだ第14章で議論するCargoそのエコシステムについては述べたいことが山ほどありますが、 とりあえずは、これで知っておくべきことは全てです。 Cargoのおかげでライブラリはとても簡単に再利用ができるので、 Rustaceanは数多くのパッケージから構成された小規模のプロジェクトを書くことができるのです。

乱数を生成する

Cargo.tomlrandクレートを追加したので、randクレートを使用開始しましょう。 次のステップは、リスト2-3のようにsrc/main.rsファイルを更新することです。

ファイル名: src/main.rs

use std::io;
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);    //秘密の数字は次の通り: {}

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

リスト2-3: 乱数を生成するコードの追加

まず、use行を追加しています: use rand::Rngですね。Rngトレイトは乱数生成器が実装するメソッドを定義していて、 このトレイトがスコープにないと、メソッドを使用できないのです。トレイトについて詳しくは、 第10章で解説します。

Rngトレイトとはメソッドを定義するもの

次に、途中に2行を追加しています。rand::thread_rng関数は、これから使う特定の乱数生成器を返してくれます: この乱数生成器は、実行スレッドに固有で、OSにより、シード値を与えられています。 そして、この乱数生成器のgen_rangeメソッドを呼び出しています。このメソッドは、 use rand::Rng文でスコープに導入したRngトレイトで定義されていますgen_rangeメソッドは二つの数字を引数に取り、 それらの間の乱数を生成してくれます。範囲は下限値を含み、上限値を含まないため、1101と指定しないと1から100の範囲の数字は得られません。

注釈: 単純に使用すべきトレイトと、クレートからどのメソッドと関数を呼び出すか知っているわけではないでしょう。 クレートの使用方法は、各クレートのドキュメントにあります。Cargoの別の素晴らしい機能は、 cargo doc --openコマンドを走らせてローカルに存在する依存すべてのドキュメントをビルドし、ブラウザで閲覧できる機能です。 例えば、randクレートの他の機能に興味があるなら、cargo doc --openコマンドを走らせて、 左側のサイドバーからrandをクリックしてください。

コードに追加した2行目は、秘密の数字を出力してくれます。これは、プログラムを開発中にはテストするのに役立ちますが、 最終版からは削除する予定です。プログラムがスタートと同時に答えを出力しちゃったら、ゲームになりませんからね!

試しに何回かプログラムを走らせてみてください:

cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 2.53 secs
     Running `target/debug/guessing_game`
Guess the number!                         (何回も出ているので、ここでは和訳は省略します)
The secret number is: 7
Please input your guess.
4
You guessed: 4
$ cargo run
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 83
Please input your guess.
5
You guessed: 5

毎回異なる乱数が出て、その数字はすべて1から100の範囲になるはずです。よくやりました!

予想と秘密の数字を比較する

今や、ユーザ入力と乱数生成ができるようになったので、比較することができますね。 このステップはリスト2-4に示されています。これから説明するように、このコードは現状ではコンパイルできないことに注意してください。

ファイル名: src/main.rs

use std::io;
use std::cmp::Ordering;
use rand::Rng;

fn main() {

    // ---snip---

    println!("You guessed: {}", guess);

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),       //小さすぎ!
        Ordering::Greater => println!("Too big!"),      //大きすぎ!
        Ordering::Equal => println!("You win!"),        //やったね!
    }
}

リスト2-4: 2値比較の可能性のある返り値を処理する

最初の新しい点は、別のuse文です。これで、std::cmp::Orderingというを標準ライブラリからスコープに導入しています。 Resultと同じくOrderingもenumです。ただ、Orderingの列挙子は、 LessGreaterEqualです。これらは、2値比較した時に発生しうる3種類の結果です。

match guess.cmp(&secret_number) {
    Ordering::Less => println!("Too small!"),
    Ordering::Greater => println!("Too big!"),
    Ordering::Equal => println!("You win!"),
}

それから、一番下に新しく5行追加してOrdering型を使用しています。cmpメソッドは、 2値を比較し、比較できるものに対してなら何に対しても呼び出せます。このメソッドは、 比較したいものへの参照を取ります: ここでは、guess変数とsecret_number変数を比較しています。 それからこのメソッドはuse文でスコープに導入したOrdering列挙型の値を返します。 matchを使用して、guess変数とsecret_numbercmpに渡して返ってきたOrderingの列挙子に基づき、 次の動作を決定しています。

match式は、複数のアーム(腕)からできています。一つのアームは、 パターンとそのパターンにmatch式の冒頭で与えた値がマッチした時に走るコードから構成されています。Rustは、 matchに与えられた値を取り、各アームのパターンを順番に照合していきます。match式とパターンは、 コードを書く際に出くわす様々なシチュエーションを表現させてくれ、 すべてのシチュエーションに対処していることを保証するのを手助けしてくれるRustの強力な機能です。 これらの機能は、それぞれ、第6章と第18章で詳しく講義することにします。

ここで使われているmatch式でどんなことが起こるかの例をじっくり観察してみましょう!例えば、 ユーザは50と予想し、ランダム生成された秘密の数字は今回、38だったとしましょう。コードが50と38を比較すると、 cmpメソッドはOrdering::Greaterを返します。50は38よりも大きいからですね。 match式にOrdering::Greaterが与えられ、各アームのパターンを吟味し始めます。まず、 最初のアームのパターンと照合します(Ordering::Lessですね)。しかし、 値のOrdering::GreaterOrdering::Lessはマッチしないため、このアームのコードは無視され、 次のアームに移ります。次のアームのパターン、Ordering::Greater見事にOrdering::Greaterとマッチします! このアームに紐づけられたコードが実行され、画面にToo big!が表示されます。 これでmatch式の実行は終わりになります。この筋書きでは、最後のアームと照合する必要はもうないからですね。

ところが、リスト2-4のコードは、まだコンパイルが通りません。試してみましょう:

$ cargo build
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
error[E0308]: mismatched types          (型が合いません)
  --> src/main.rs:23:21
   |
23 |     match guess.cmp(&secret_number) {
   |                     ^^^^^^^^^^^^^^ expected struct `std::string::String`, found integral variable
   |                                    (構造体`std::string::String`を予期したけど、整数型変数が見つかりました)
   |
   = note: expected type `&std::string::String`
   = note:    found type `&{integer}`

error: aborting due to previous error   (先のエラーのため、処理を中断します)
Could not compile `guessing_game`.      (`guessing_game`をコンパイルできませんでした)

このエラーの核は、型の不一致があると言っています。Rustには、強い静的型システムがあります。 しかし、型推論にも対応しています。let guess = String::new()と書いた時、コンパイラは、 guessString型であるはずと推論してくれ、その型を明示させられることはありませんでした。 一方で、secret_number変数は、数値型です。1から100を表すことができる数値型はいくつかあります: i32は32ビットの数字; u32は32ビットの非負数字; i64は64ビットの数字などです。 Rustでの標準は、i32であり、型情報をどこかに追加して、コンパイラに異なる数値型だと推論させない限り、 secret_numberの型はこれになります。エラーの原因は、Rustでは、文字列と数値型を比較できないことです。

究極的には、プログラムが入力として読み込むString型を現実の数値型に変換し、 予想と数値として比較できるようにしたいわけです。これは、以下の2行をmain関数の本体に追記することでできます:

ファイル名: src/main.rs

// --snip--

    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!"),
    }
}

その2行とは:

let guess: u32 = guess.trim().parse()
    .expect("Please type a number!");

guessという名前の変数を生成しています。あれ、でも待って。もうプログラムにはguessという名前の変数がありませんでしたっけ? 確かにありますが、Rustでは、新しい値でguessの値を覆い隠す(shadow)ことが許されているのです。 この機能は、値を別の型に変換したいシチュエーションでよく使われます。 シャドーイング(shadowing)のおかげで別々の変数を2つ作らされることなく、guessという変数名を再利用することができるのです。 guess_strguessみたいなね(シャドーイングについては、第3章でもっと掘り下げます)。

guessguess.trim().parse()という式に束縛しています。この式中のguessは、 入力が入ったString型の元々のguessを指しています。Stringオブジェクトのtrimメソッドは、 両端の空白をすべて除去します。u32型は、数字しか含むことができませんが、ユーザは、 read_lineの処理を終えるためにエンターを押さなければなりません。 ユーザがエンターを押したら、改行文字が文字列に追加されます。 具体例として、ユーザが5を入力して、 エンターを押せば、guessは次のようになります: 5\n。 この\nが「改行」、つまりエンターキーを押した結果を表しているわけです。 trimメソッドは、\nを削除するので、ただの5になります。

型が違うときは、シャドウイングして型を上書きできる。

.trim()で両端の空白を削除し、.parse()で文字列を解析させて、自動で型を何らかの数字にする。そのとき、u32を指定したいので、左側でu32を指定している。

文字列のparseメソッドは、文字列を解析して何らかの数値にします。 このメソッドは、いろんな数値型を解析できるので、let guess: u32としてコンパイラに私たちが求めている型をズバリ示唆する必要があるのです。 guessの後のコロン(:)がコンパイラに変数の型を注釈する合図になります。 Rustには、組み込みの数値型がいくつかあります; ここのu32型は、32ビットの非負整数です。 u32型は小さな非負整数のデフォルトの選択肢として丁度良いです。他の数値型については、第3章で学ぶでしょう。 付け加えると、このサンプルプログラムのu32という注釈とsecret_number変数との比較は、 secret_number変数もu32型であるとコンパイラが推論することを意味します。 従って、今では比較が同じ型の2つの値で行われることになるわけです!

parseメソッドの呼び出しは、エラーになりやすいです。例としては、文字列がA👍%を含んでいたら、 数値に変換できるわけがありません。失敗する可能性があるので、parseメソッドは、 Result型を返すわけです。ちょうど、(「Result型で失敗する可能性に対処する」節で先ほど議論した)read_lineメソッドのようにというわけですね。 今回も、expectメソッドを使用してResult型を同じように扱います。このResultexpectメソッドを再度使用して、 同じように扱います。もし、文字列から数値を生成できなかったために、parseメソッドがResult型のErr列挙子を返したら、 expectメソッドの呼び出しは、ゲームをクラッシュさせ、与えたメッセージを表示します。 もし、parseメソッドが文字列の数値への変換に成功したら、Result型のOk列挙子を返し、 expectメソッドは、Ok値から必要な数値を返してくれます。

さあ、プログラムを走らせましょう!

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 0.43 secs
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 58
Please input your guess.
  76
You guessed: 76
Too big!

いいですね!予想の前にスペースを追加したにもかかわらず、プログラムはちゃんとユーザが76と予想したことを導き出しました。 プログラムを何回か走らせて、異なる入力の色々な振る舞いを確認してください: つまり、 数字を正しく言い当てたり、大きすぎる値を予想したり、小さすぎる数字を入力したりということです。

ここまでで大方ゲームはうまく動くようになりましたが、まだユーザは1回しか予想できません。 ループを追加して、その部分を変更しましょう!

ループで複数回の予想を可能にする

loopキーワードは、無限ループを作り出します。これを追加して、ユーザが何回も予想できるようにしましょう:

ファイル名: src/main.rs

// --snip--

    println!("The secret number is: {}", secret_number);

    loop {
        println!("Please input your guess.");

        // --snip--

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => println!("You win!"),
        }
    }
}

見てわかる通り、予想入力部分以降をループに入れ込みました。ループ内の行にインデントを追加するのを忘れないようにして、 またプログラムを走らせてみましょう。新たな問題が発生したことに注目してください。 プログラムが教えた通りに動作しているからですね: 永遠に予想入力を求めるわけです! これでは、ユーザが終了できないようです!

ユーザは、ctrl-cというキーボードショートカットを使って、いつでもプログラムを強制終了させられます。 しかし、「予想と秘密の数字を比較する」節のparseメソッドに関する議論で触れたように、 この貪欲なモンスターを回避する別の方法があります: ユーザが数字以外の答えを入力すれば、プログラムはクラッシュするのです。 ユーザは、その利点を活かして、終了することができます。以下のようにですね:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 1.50 secs
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 59
Please input your guess.
45
You guessed: 45
Too small!
Please input your guess.
60
You guessed: 60
Too big!
Please input your guess.
59
You guessed: 59
You win!
Please input your guess.
quit
thread 'main' panicked at 'Please type a number!: ParseIntError { kind: InvalidDigit }', src/libcore/result.rs:785
(スレッド'main'は'数字を入力してください!: ParseIntError { kind: InvalidDigit }', src/libcore/result.rs:785でパニックしました)
note: Run with `RUST_BACKTRACE=1` for a backtrace.
(注釈: `RUST_BACKTRACE=1`で走らせるとバックトレースを見れます)
error: Process didn't exit successfully: `target/debug/guess` (exit code: 101)
(エラー: プロセスは予期なく終了しました)

quitと入力すれば、実際にゲームを終了できるわけですが、別に他の数字以外の入力でもそうなります。 しかしながら、これは最低限度と言えるでしょう。正しい数字が予想されたら、自動的にゲームが停止してほしいわけです。

正しい予想をした後に終了する

break文を追加して、ユーザが勝った時にゲームが終了するようにプログラムしましょう:

ファイル名: src/main.rs

// --snip--

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

break文の1行をYou win!の後に追記することで、ユーザが秘密の数字を正確に予想した時に、 プログラムはループを抜けるようになりました。ついでに、ループを抜けることは、プログラムを終了することを意味します。 ループがmain関数の最後の部分だからですね。

不正な入力を処理する

さらにゲームの振る舞いを改善するために、ユーザが数値以外を入力した時にプログラムをクラッシュさせるのではなく、 非数値を無視してユーザが数当てを続けられるようにしましょう!これは、 guessString型からu32型に変換される行を改変することで達成できます。リスト2-5のようにですね。

ファイル名: src/main.rs

// --snip--

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

// --snip--

リスト2-5: 非数値の予想を無視し、プログラムをクラッシュさせるのではなく、もう1回予想してもらう

expectメソッドの呼び出しからmatch式に切り替えることは、 エラーでクラッシュする動作からエラー処理を行う処理に変更する一般的な手段になります。parseメソッドは、 Result型を返し、ResultOkErrの列挙子を取りうる列挙型であることを思い出してください。 ここではmatch式を使っています。cmpメソッドのOrderingという結果のような感じですね。

parseメソッドは、文字列から数値への変換に成功したら、結果の数値を保持するOk値を返します。 このOk値は、最初のアームのパターンにマッチし、このmatch式はparseメソッドが生成し、 Ok値に格納したnumの値を返すだけです。その数値が最終的に、生成している新しいguess変数として欲しい場所に存在します。

parseメソッドは、文字列から数値への変換に失敗したら、エラーに関する情報を多く含むErr値を返します。 このErr値は、最初のmatchアームのOk(num)というパターンにはマッチしないものの、 2番目のアームのErr(_)というパターンにはマッチするわけです。この_は、包括値です; この例では、 保持している情報がどんなものでもいいから全てのErr値にマッチさせたいと宣言しています。 従って、プログラムは2番目のアームのコードを実行し(continueですね)、これは、 loopの次のステップに移り、再度予想入力を求めるようプログラムに指示します。故に実質的には、 プログラムはparseメソッドが遭遇しうる全てのエラーを無視するようになります!

さて、プログラムの全てがうまく予想通りに動くはずです。試しましょう:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 61
Please input your guess.
10
You guessed: 10
Too small!
Please input your guess.
99
You guessed: 99
Too big!
Please input your guess.
foo
Please input your guess.
61
You guessed: 61
You win!

素晴らしい!最後にひとつまみ変更を加えて、数当てゲームを完了にしましょう。 プログラムが未だに秘密の数字を出力していることを思い出してください。テスト中はうまく動くけど、 ゲームを台無しにしてしまいます。秘密の数字を出力するprintln!を削除しましょう。 リスト2-6が成果物のコードです:

ファイル名: src/main.rs

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

リスト2-6: 数当てゲームの完全なコード

まとめ

ここまでで、数当てゲームの構築に成功しました。おめでとうございます!

このプロジェクトは、たくさんの新しいRustの概念に触れる実践的な方法でした: letmatch、メソッド、関連関数、外部クレートの使用などなど。 以降の数章で、これらの概念についてより深く学ぶことになるでしょう。 第3章では、ほとんどのプログラミング言語に存在する、変数、データ型、関数などの概念について講義し、 それらのRustでの使用方法について示します。 第4章では、所有権について見ます。これにより、Rustは他の言語とかけ離れた存在になっています。 第5章では、構造体とメソッド記法について議論し、第6章ではenumの動作法を説明します。

ABOUT ME
たけ
はじめまして! たけといいます。 20代男性サラリーマンが資産運用で5年で3000万をめざします。 これを読んで自分でも出来るのではないかと思ってくれる人が増えると嬉しいです。 お金を得ることは手段に過ぎません。若いうちに稼いで、自分の時間をより大切なことに使いたいです。 【2019投資戦歴】 投資資金合計 300万 2019年度単年損益(年利) FX 15万(15%) 投信 9万(7%) 株式 4万(8%) ※投信、株式は含み益