数当てゲームのプログラミング

ハンズオン形式のプロジェクトに一緒に取り組むことで、Rustの世界に飛び込んでみましょう!  この章ではRustの一般的な概念を、実際のプログラムでの使い方を示しながら紹介します。 letmatch、メソッド、関連関数、外部クレートの使いかたなどについて学びます!  これらについての詳細は後続の章で取り上げますので、この章では基本的なところを練習します。

プログラミング初心者向けの定番問題である「数当てゲーム」を実装してみましょう。 これは次のように動作します。 プログラムは1から100までのランダムな整数を生成します。 そして、プレーヤーに予想(した数字)を入力するように促します。 予想が入力されると、プログラムはその予想が小さすぎるか大きすぎるかを表示します。 予想が当たっているなら、お祝いのメッセージを表示し、ゲームを終了します。

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

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

$ cargo new guessing_game
$ cd guessing_game

最初のコマンドcargo newは、第1引数としてプロジェクト名 (guessing_game) を取ります。 2番目のコマンドは新規プロジェクトのディレクトリに移動します。

生成されたCargo.tomlファイルを見てみましょう。

ファイル名:Cargo.toml

[package]
name = "guessing_game"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

第1章で見たようにcargo newは「Hello, world!」プログラムを生成してくれます。 src/main.rsファイルをチェックしてみましょう。

ファイル名:src/main.rs

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

さて、cargo runコマンドを使って、この「Hello, world!」プログラムのコンパイルと実行を一気に行いましょう。

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 1.50s
     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:ユーザに予想を入力してもらい、それを出力するコード

このコードには多くの情報が詰め込まれています。 行ごとに見ていきましょう。 ユーザ入力を受け付け、結果を出力するためにはio(入出力)ライブラリをスコープに入れる必要があります。 ioライブラリは、stdと呼ばれる標準ライブラリに含まれています。

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

Rustはデフォルトで、標準ライブラリで定義されているアイテムの中のいくつかを、すべてのプログラムのスコープに取り込みます。 このセットはprelude(プレリュード)と呼ばれ、標準ライブラリのドキュメントでその中のすべてを見ることができます。

使いたい型がpreludeにない場合は、その型をuse文で明示的にスコープに入れる必要があります。 std::ioライブラリをuseすると、ユーザ入力を受け付ける機能など(入出力に関する)多くの便利な機能が利用できるようになります。

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

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

fn構文は関数を新しく宣言し、かっこの()は引数がないことを示し、波括弧の{は関数の本体を開始します。

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

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

このコードはゲームの内容などを示すプロンプトを表示し、ユーザに入力を求めています。

値を変数に保持する

次に、ユーザの入力を格納するための変数を作りましょう。 こんな感じです。

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

プログラムが少し興味深いものになってきました。 この小さな行の中でいろいろなことが起きています。 let文を使って変数を作っています。 別の例も見てみましょう。

let apples = 5;

この行ではapplesという名前の新しい変数を作成し5という値に束縛しています。 Rustでは変数はデフォルトで不変(immutable)になります。 この概念については第3章の「変数と可変性」の節で詳しく説明します。 変数を可変(mutable)にするには、変数名の前にmutをつけます。

let apples = 5; // immutable
                // 不変
let mut bananas = 5; // mutable
                     // 可変

注://構文は行末まで続くコメントを開始し、Rustはコメント内のすべて無視します。 コメントについては第3章で詳しく説明します。

数当てゲームのプログラムに戻りましょう。 ここまでの話でlet mut guessguessという名前の可変変数を導入することがわかったと思います。 等号記号(=)はRustに、いまこの変数を何かに束縛したいことを伝えます。 等号記号の右側にはguessが束縛される値があります。 これはString::new関数を呼び出すことで得られた値で、この関数はString型の新しいインスタンスを返します。 Stringは標準ライブラリによって提供される文字列型で、サイズが拡張可能な、UTF-8でエンコードされたテキスト片になります。

::newの行にある::構文はnewString型の関連関数であることを示しています。 関連関数とは、ある型(ここではString)に対して実装される関数のことです。 このnew関数は新しい空の文字列を作成します。 new関数は多くの型に見られます。 なぜなら、何らかの新しい値を作成する関数によくある名前だからです。

つまりlet mut guess = String::new();という行は可変変数を作成し、その変数は現時点では新しい空のStringのインスタンスに束縛されているわけです。 ふう!

ユーザの入力を受け取る

プログラムの最初の行にuse std::ioと書いて、標準ライブラリの入出力機能を取り込んだことを思い出してください。 ここでioモジュールのstdin関数を呼び出して、ユーザ入力を処理できるようにしましょう。

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

もし、プログラムの最初にuse std::ioと書いてioライブラリをインポートしていなかったとしても、std::io::stdinのように呼び出せば、この関数を利用できます。 stdin関数はターミナルの標準入力へのハンドルを表す型であるstd::io::Stdinのインスタンスを返します。

次の.read_line(&mut guess)行は、標準入力ハンドルのread_lineメソッドを呼び出し、ユーザからの入力を得ています。 また、read_lineの引数として&mut guessを渡し、ユーザ入力をどの文字列に格納するかを指示しています。 read_lineメソッドの仕事は、ユーザが標準入力に入力したものを文字列に(いまの内容を上書きせずに)追加することですので、文字列を引数として渡しているわけです。 引数の文字列は、その内容をメソッドが変更できるように、可変である必要があります。

この&は、この引数が参照であることを示し、これによりコードの複数の部分が同じデータにアクセスしても、そのデータを何度もメモリにコピーしなくて済みます。 参照は複雑な機能(訳注:一部のプログラム言語では正しく使うのが難しい機能)ですが、Rustの大きな利点の一つは参照を安全かつ簡単に使用できることです。 このプログラムを完成させるのに、そのような詳細を知る必要はないしょう。 とりあえず知っておいてほしいのは、変数のように参照もデフォルトで不変であることです。 したがって、&guessではなく&mut guessと書いて可変にする必要があります。 (参照については第4章でより詳しく説明します)

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

まだ、このコードの行は終わってません。 これから説明するのはテキスト上は3行目になりますが、まだ一つの論理的な行の一部分に過ぎません。 次の部分はこのメソッドです。

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

このコードは、こう書くこともできました。

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

しかし、長い行は読みづらいので分割したほうがよいでしょう。 .method_name()構文でメソッドを呼び出すとき、長い行を改行と空白で分割するのが賢明なことがよくあります。 それでは、この行(expect()メソッド)が何をするのか説明します。

前述したように、read_lineメソッドは渡された文字列にユーザが入力したものを入れます。 しかし、同時に値(この場合はio::Result)も返します。 Rustの標準ライブラリにはResultという名前の型がいくつかあります。 汎用のResultと、io::Resultといったサブモジュール用の特殊な型などです。 これらのResult型は列挙型になります。 列挙型はenumとも呼ばれ、取りうる値として決まった数の列挙子(variant)を持ちます。 列挙型はよくmatchと一緒に使われます。 これは条件式の一種で、評価時に、列挙型の値がどの列挙子であるかに基づいて異なるコードを実行できるという便利なものです。

enumについては第6章で詳しく説明します。 これらのResult型の目的は、エラー処理に関わる情報を符号化(エンコード)することです。

Resultの列挙子はOkErrです。 Ok列挙子は処理が成功したことを示し、Okの中には正常に生成された値が入っています。 Err列挙子は処理が失敗したことを意味し、Errには処理が失敗した過程や理由についての情報が含まれています。

Result型の値にも、他の型と同様にメソッドが定義されています。 io::Resultのインスタンスにはexpectメソッドがありますので、これを呼び出せます。 このio::ResultインスタンスがErrの値の場合、expectメソッドはプログラムをクラッシュさせ、引数として渡されたメッセージを表示します。 read_lineメソッドがErrを返したら、それはおそらく基礎となるオペレーティング・システムに起因するものでしょう。 もしこのio::ResultオブジェクトがOk値の場合、expectメソッドはOk列挙子が保持する戻り値を取り出して、その値だけを返してくれます。 こうして私たちはその値を使うことができるわけです。 今回の場合、その値はユーザ入力のバイト数になります。

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

$ cargo build
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
warning: unused `Result` that 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
   = note: this `Result` may be an `Err` variant, which should be handled

warning: `guessing_game` (bin "guessing_game") generated 1 warning
    Finished dev [unoptimized + debuginfo] target(s) in 0.59s

Rustは私たちがread_lineから返されたResult値を使用していないことを警告し、これはプログラムがエラーの可能性に対処していないことを示します。

警告を抑制する正しい方法は実際にエラー処理を書くことです。 しかし、現時点では問題が起きたときにこのプログラムをクラッシュさせたいだけなので、expectが使えるわけです。 エラーからの回復については第9章で学びます。

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

閉じ波かっこを除けば、ここまでのコードで説明するのは残り1行だけです。

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組目は2番目の値、というように続いていきます。 一回のprintln!の呼び出しで複数の値を表示するなら次のようになります。


#![allow(unused)]
fn main() {
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 6.44s
     Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
6
You guessed: 6

これで、キーボードからの入力を得て、それを表示するという、ゲームの最初の部分は完成になります。

秘密の数字を生成する

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

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

クレートはRustソースコードを集めたものであることを思い出してください。 私たちがここまで作ってきたプロジェクトはバイナリクレートであり、これは実行可能ファイルになります。 randクレートはライブラリクレートです。 他のプログラムで使用するためのコードが含まれており、単独で実行することはできません。

Cargoがその力を発揮するのは外部クレートと連携するときです。 randを使ったコードを書く前に、Cargo.tomlファイルを編集してrandクレートを依存関係に含める必要があります。 そのファイルを開いて、Cargoが作ってくれた[dependencies]セクションヘッダの下に次の行を追加してください。 バージョンナンバーを含め、ここに書かれている通り正確にrandを指定してください。 そうしないと、このチュートリアルのコード例が動作しないかもしれません。

ファイル名:Cargo.toml

rand = "0.8.3"

Cargo.tomlファイルでは、ヘッダに続くものはすべて、他のセクションが始まるまで続くセクションの一部になります。 (訳注:Cargo.tomlファイル内には複数のセクションがあり、各セクションは[ ]で囲まれたヘッダ行から始まります)

[dependecies]はプロジェクトが依存する外部クレートと必要とするバージョンをCargoに伝えます。 今回はrandクレートを0.8.3というセマンティックバージョン指定子で指定します。 CargoはセマンティックバージョニングSemVerと呼ばれることもあります)を理解しており、これはバージョンナンバーを記述するための標準です。 0.8.3という数字は実際には^0.8.3の省略記法で、0.8.3以上0.9.0未満の任意のバージョンを意味します。 Cargoはこれらのバージョンを、バージョン0.8.3と互換性のある公開APIを持つものとみなします。 この仕様により、この章のコードが引き続きコンパイルできるようにしつつ、最新のパッチリリースを取得できるようになります。 0.9.0以降のバージョンは、以下の例で使用しているものと同じAPIを持つことを保証しません。

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

$ cargo build
    Updating crates.io index
    (crates.ioインデックスを更新しています)
  Downloaded rand v0.8.3
  (rand v0.8.3をダウンロードしています)
  Downloaded libc v0.2.86
  Downloaded getrandom v0.2.2
  Downloaded cfg-if v1.0.0
  Downloaded ppv-lite86 v0.2.10
  Downloaded rand_chacha v0.3.0
  Downloaded rand_core v0.6.2
   Compiling rand_core v0.6.2
   (rand_core v0.6.2をコンパイルしています)
   Compiling libc v0.2.86
   Compiling getrandom v0.2.2
   Compiling cfg-if v1.0.0
   Compiling ppv-lite86 v0.2.10
   Compiling rand_chacha v0.3.0
   Compiling rand v0.8.3
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
   (guessing_game v0.1.0をコンパイルしています)
    Finished dev [unoptimized + debuginfo] target(s) in 2.53s

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

もしかしたら異なるバージョンナンバー(とはいえ、SemVerのおかげですべてのコードに互換性があります)や、 異なる行(オペレーティングシステムに依存します)が表示されるかもしれません。 また、行の順序も違うかもしれません。

外部依存を持つようになると、Cargoはその依存関係が必要とするすべてについて最新のバージョンをレジストリから取得します。 レジストリとはCrates.ioのデータのコピーです。 Crates.ioは、Rustのエコシステムにいる人たちがオープンソースのRustプロジェクトを投稿し、他の人が使えるようにする場所です。

レジストリの更新後、Cargoは[dependencies]セクションにリストアップされているクレートをチェックし、まだ取得していないものがあればダウンロードします。 ここでは依存関係としてrandだけを書きましたが、randが動作するために依存している他のクレートも取り込まれています。 クレートをダウンロードしたあと、Rustはそれらをコンパイルし、依存関係が利用できる状態でプロジェクトをコンパイルします。

何も変更せずにすぐに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.8.4が出て、そのバージョンには重要なバグ修正が含まれていますが、同時にあなたのコードを破壊するリグレッションも含まれているとします。 これに対応するため、Rustはcargo buildを最初に実行したときにCargo.lockファイルを作成します。 (いまのguessing_gameディレクトリにもあるはずです)

プロジェクトを初めてビルドするとき、Cargoは条件に合うすべての依存関係のバージョンを計算しCargo.lockファイルに書き込みます。 次にプロジェクトをビルドすると、CargoはCargo.lockファイルが存在することを確認し、バージョンを把握するすべての作業を再び行う代わりに、そこで指定されているバージョンを使います。 これにより再現性のあるビルドを自動的に行えます。 言い換えれば、Cargo.lockファイルのおかげで、あなたが明示的にアップグレードするまで、プロジェクトは0.8.3を使い続けます。

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

クレートを本当にアップグレードしたくなったときのために、Cargoはupdateコマンドを提供します。 このコマンドはCargo.lockファイルを無視して、Cargo.tomlファイル内の全ての指定に適合する最新バージョンを算出します。 成功したらCargoはそれらのバージョンをCargo.lockファイルに記録します。 ただし、デフォルトでCargoは0.8.3以上、0.9.0未満のバージョンのみを検索します。 もしrandクレートの新しいバージョンとして0.8.40.9.0の二つがリリースされていたなら、cargo updateを実行したときに以下のようなメッセージが表示されるでしょう。

$ cargo update
    Updating crates.io index
    (crates.ioインデックスを更新しています)
    Updating rand v0.8.3 -> v0.8.4
    (randクレートをv0.8.3 -> v0.8.4に更新しています)

Cargoは0.9.0リリースを無視します。 またそのとき、Cargo.lockファイルが変更され、randクレートの現在使用中のバージョンが0.8.4になったことにも気づくでしょう。 そうではなく、randのバージョン0.9.0か、0.9.x系のどれかを使用するには、Cargo.tomlファイルを以下のように変更する必要があります。

[dependencies]

rand = "0.9.0"

次にcargo buildコマンドを実行したとき、Cargoは利用可能なクレートのレジストリを更新し、あなたが指定した新しいバージョンに従ってrandの要件を再評価します。

Cargoそのエコシステムについては、まだ伝えたいことが山ほどありますが、それらについては第14章で説明します。 いまのところは、これだけ知っていれば十分です。 Cargoはライブラリの再利用をとても簡単にしてくれるので、Rustaceanが数多くのパッケージから構成された小さなプロジェクトを書くことが可能になっています。

乱数を生成する

randクレートを使って予想する数字を生成しましょう。 次のステップはsrc/main.rsファイルをリスト2-3のように更新することです。

ファイル名: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 rand::Rngという行を追加します。 Rngトレイトは乱数生成器が実装すべきメソッドを定義しており、それらのメソッドを使用するには、このトレイトがスコープ内になければなりません。 トレイトについて詳しくは第10章で解説します。

次に、途中に2行を追加しています。 最初の行ではrand::thread_rng関数を呼び出して、これから使う、ある特定の乱数生成器を取得しています。 なお、この乱数生成器は現在のスレッドに固有で、オペレーティングシステムからシード値を得ています。 そして、この乱数生成器のgen_rangeメソッドを呼び出しています。 このメソッドはuse rand::Rng文でスコープに導入したRngトレイトで定義されています。 gen_rangeメソッドは範囲式を引数にとり、その範囲内の乱数を生成してくれます。 ここで使っている範囲式の種類は開始..終了という形式で、下限値は含みますが上限値は含みません。 そのため、1から100までの数をリクエストするには1..101と指定する必要があります。 あるいは、これと同等の1..=100という範囲を渡すこともできます。

注:クレートのどのトレイトをuseするかや、どのメソッドや関数を呼び出すかを知るために、各クレートにはその使い方を説明したドキュメントが用意されています。 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.53s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 7
Please input your guess.
4
You guessed: 4

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     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 rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    // --snip--
    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);

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

リスト2-4:二つの数値を比較したときに返される可能性のある値を処理する

まずuse文を追加して標準ライブラリからstd::cmp::Orderingという型をスコープに導入しています。 Orderingもenumの一つでLessGreaterEqualという列挙子を持っています。 これらは二つの値を比較したときに得られる3種類の結果です。

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

それからOrdering型を使用する新しい5行をいちばん下に追加してしています。 cmpメソッドは二つの値の比較を行い、比較できるものになら何に対しても呼び出せます。 比較対象への参照をとり、ここではguesssecret_numberを比較しています。 そしてuse文でスコープに導入したOrdering列挙型の列挙子を返します。 ここではmatch式を使用しており、guesssecret_numberの値に対してcmpを呼んだ結果返されたOrderingの列挙子に基づき、次の動作を決定しています。

match式は複数のアーム(腕)で構成されます。 各アームはマッチさせるパターンと、matchに与えられた値がそのアームのパターンにマッチしたときに実行されるコードで構成されます。 Rustはmatchに与えられた値を受け取って、各アームのパターンを順に照合していきます。 パターンとmatch式はRustの強力な機能で、コードか遭遇する可能性のあるさまざまな状況を表現し、それらすべてを確実に処理できるようにします。 これらの機能については、それぞれ第6章と第18章で詳しく説明します。

ここで使われているmatch式に対して、例を通して順に見ていきましょう。 たとえばユーザが50と予想し、今回ランダムに生成された秘密の数字は38だったとしましょう。 コードが50と38を比較すると、50は38よりも大きいのでcmpメソッドはOrdering::Greaterを返します。 match式はOrdering::Greaterの値を取得し、各アームのパターンを吟味し始めます。 まず最初のアームのパターンであるOrdering::Lessを見て、Ordering::Greaterの値とOrdering::Lessがマッチしないことがわかります。 そのため、このアームのコードは無視して、次のアームに移ります。 次のアームのパターンはOrdering::Greaterで、これはOrdering::Greaterマッチします!  このアームに関連するコードが実行され、画面にToo big!と表示されます。 このシナリオでは最後のアームと照合する必要がないためmatch式(の評価)は終了します。

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

$ cargo build
   Compiling libc v0.2.86
   Compiling getrandom v0.2.2
   Compiling cfg-if v1.0.0
   Compiling ppv-lite86 v0.2.10
   Compiling rand_core v0.6.2
   Compiling rand_chacha v0.3.0
   Compiling rand v0.8.3
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
error[E0308]: mismatched types          (型が合いません)
  --> src/main.rs:22:21
   |
22 |     match guess.cmp(&secret_number) {
   |                     ^^^^^^^^^^^^^^ expected struct `String`, found integer
   |                                    (構造体`std::string::String`を予期したけど、整数型変数が見つかりました)
   |
   = note: expected reference `&String`
              found reference `&{integer}`

error[E0283]: type annotations needed for `{integer}`
   --> src/main.rs:8:44
    |
8   |     let secret_number = rand::thread_rng().gen_range(1..101);
    |         -------------                      ^^^^^^^^^ cannot infer type for type `{integer}`
    |         |
    |         consider giving `secret_number` a type
    |
    = note: multiple `impl`s satisfying `{integer}: SampleUniform` found in the `rand` crate:
            - impl SampleUniform for i128;
            - impl SampleUniform for i16;
            - impl SampleUniform for i32;
            - impl SampleUniform for i64;
            and 8 more
note: required by a bound in `gen_range`
   --> /Users/carolnichols/.cargo/registry/src/github.com-1ecc6299db9ec823/rand-0.8.3/src/rng.rs:129:12
    |
129 |         T: SampleUniform,
    |            ^^^^^^^^^^^^^ required by this bound in `gen_range`
help: consider specifying the type arguments in the function call
    |
8   |     let secret_number = rand::thread_rng().gen_range::<T, R>(1..101);
    |                                                     ++++++++

Some errors have detailed explanations: E0283, E0308.
For more information about an error, try `rustc --explain E0283`.
error: could not compile `guessing_game` due to 2 previous errors      (先の2つのエラーのため、`guessing_game`をコンパイルできませんでした)

このエラーの核心は型の不一致があると述べていることです。 Rustは強い静的型システムを持ちますが、型推論も備えています。 let guess = String::new()と書いたとき、RustはguessString型であるべきと推論したので、私たちはその型を書かずに済みました。 一方でsecret_numberは数値型です。 Rustのいくつかの数値型は1から100までの値を表現でき、それらの型には32ビット数値のi32、符号なしの32ビット数値のu32、64ビット数値のi64などがあります。 Rustのデフォルトはi32型で、型情報をどこかに追加してRustに異なる数値型だと推論させない限りsecret_numberの型はこれになります。 エラーの原因はRustが文字列と数値型を比較できないためです。

最終的にはプログラムが入力として読み込んだStringを実数型に変換し、秘密の数字と数値として比較できるようにしたいわけです。 そのためにはmain関数の本体に次の行を追加します。

ファイル名: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);

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

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

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

その行とはこれのことです。

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

guessという名前の変数を作成しています。 しかし待ってください、このプログラムには既にguessという名前の変数がありませんでしたか?  たしかにありますが、Rustではguessの前の値を新しい値で覆い隠す(shadowする)ことが許されているのです。 シャドーイング(shadowing)は、guess_strguessのような重複しない変数を二つ作る代わりに、guessという変数名を再利用させてくれるのです。 これについては第3章で詳しく説明しますが、今のところ、この機能はある型から別の型に値を変換するときによく使われることを知っておいてください。

この新しい変数をguess.trim().parse()という式に束縛しています。 式の中にあるguessは、入力が文字列として格納されたオリジナルのguess変数を指しています。 Stringインスタンスのtrimメソッドは文字列の先頭と末尾の空白をすべて削除します。 これは数値データのみを表現できるu32型とこの文字列を比較するために(準備として)行う必要があります。 ユーザは予想を入力したあとread_lineの処理を終えるためにEnterキーを押す必要がありますが、これにより文字列に改行文字が追加されます。 たとえばユーザが5と入力してEnterキーを押すと、guess5\nになります。 この\nは「改行」を表しています。(WindowsではEnterキーを押すとキャリッジリターンと改行が入り\r\nとなります) trimメソッドは\n\r\nを削除するので、その結果5だけになります。

文字列のparseメソッドは文字列をパース(解析)して何らかの数値にします。 このメソッドは(文字列を)さまざまな数値型へとパースできるので、let guess: u32としてRustに正確な数値型を伝える必要があります。 guessの後にコロン(:)を付けることで変数の型に注釈をつけることをRustに伝えています。 Rustには組み込みの数値型がいくつかあります。 ここにあるu32は符号なし32ビット整数で、小さな正の数を表すデフォルトの型に適しています。 他の数値型については第3章で学びます。 さらに、このサンプルプログラムでは、u32という注釈とsecret_number変数との比較していることから、Rustはsecret_number変数もu32型であるべきだと推論しています。 つまり、いまでは二つの同じ型の値を比較することになるわけです!

parseメソッドは論理的に数値に変換できる文字にしか使えないので、よくエラーになります。 たとえば文字列にA👍%が含まれていたら数値に変換する術はありません。 解析に失敗する可能性があるため、parseメソッドはread_lineメソッドと同様にResult型を返します (Result型で失敗の可能性を扱う」で説明しました)  今回もexpectメソッドを使用してResult型を同じように扱います。 parseメソッドが文字列から数値を作成できなかったためにResult型のErr列挙子を返したら、expectの呼び出しはゲームをクラッシュさせ、私たちが与えたメッセージを表示します。 parseが文字列をうまく数値へ変換できたときはResult型のOk列挙子を返し、expectOk値から欲しい数値を返してくれます。

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

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 0.43s
     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

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

    // --snip--

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

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

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

見ての通り予想入力のプロンプト以降をすべてループ内に移動しました。 ループ内の行をさらに4つのスペースでインデントして、もう一度プログラムを実行してください。 プログラムはいつまでも推測を求めるようになりましたが、実はこれが新たな問題を引き起こしています。 これではユーザが(ゲームを)終了できません!

ユーザはキーボードショートカットのctrl-cを使えば、いつでもプログラムを中断させられます。 しかし「予想と秘密の数字を比較する」のparseで述べたように、この飽くなきモンスターから逃れる方法はもう一つあります。 ユーザが数字以外の答えを入力すればプログラムはクラッシュします。 それを利用して以下のようにすれば終了できます。

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 1.50s
     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/main.rs:28:47
(スレッド'main'は'数字を入力してください!:ParseIntError { kind: InvalidDigit }', src/libcore/result.rs:785でパニックしました)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
(注:`RUST_BACKTRACE=1`で走らせるとバックトレースを見れます)

quitと入力すればゲームが終了しますが、数字以外の入力でもそうなります。 これは控えめに言っても最適ではありません。 私たちは正しい数字が予想されたときにゲームが停止するようにしたいのです。

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

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

ファイル名: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);

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

    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 = guess.trim().parse().expect("Please type a number!");

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

        // --snip--

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

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

不正な入力を処理する

このゲームの動作をさらに洗練させるために、ユーザが数値以外を入力したときにプログラムをクラッシュさせるのではなく、数値以外を無視してユーザが数当てを続けられるようにしましょう。 これはリスト2-5のように、Stringからu32guessを変換する行を変えることで実現できます。

ファイル名: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);

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

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

        let mut guess = String::new();

        // --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--

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

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

expectの呼び出しからmatch式に切り替えて、エラーによるクラッシュからエラー処理へと移行します。 parseResult型を返すことと、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)
    Finished dev [unoptimized + debuginfo] target(s) in 4.45s
     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:数当てゲームの完全なコード

まとめ

数当てゲームを無事に作り上げることができました。 おめでとうございます!

このプロジェクトではハンズオンを通して、letmatch、メソッド、関連関数、外部クレートの使いかたなど、多くの新しいRustの概念に触れました。 以降の章では、これらの概念についてより詳しく学びます。 第3章では変数、データ型、関数など多くのプログラミング言語が持つ概念を取り上げ、Rustでの使い方を説明します。 第4章ではRustを他の言語とは異なるものに特徴づける、所有権について説明します。 第5章では構造体とメソッドの構文について説明し、第6章では列挙型がどのように動くのかについて説明します。

関連キーワード:  println, rand, 入力, Cargo, プログラム, let, read, 予想, secret, Ordering