数当てゲーム

最初のプロジェクトとして、古典的な初心者向けのプログラミングの問題、数当てゲームを実装します。 動作: プログラムは1から100の間のあるランダムな数字を生成します。 そしてその値の予想値の入力を促します。 予想値を入力すると大きすぎるあるいは小さすぎると教えてくれます。 当たったらおめでとうと言ってくれます。良さそうですか?

セットアップ

新しいプロジェクトを作りましょう。プロジェクトのディレクトリへ行って下さい。 hello_world の時にどのようなディレクトリ構成で、どのように Cargo.toml を作る必要があったか覚えてますか? Cargoにはそれらのことをしてくれるコマンドがあるのでした。使いましょう。

$ cd ~/projects
$ cargo new guessing_game --bin
$ cd guessing_game

cargo new にプロジェクトの名前と、そしてライブラリではなくバイナリを作るので --bin フラグを渡します。

生成された Cargo.toml を確認しましょう。

[package]

name = "guessing_game"
version = "0.1.0"
authors = ["あなたの名前 <you@example.com>"]

Cargoはこれらの情報を環境から取得します。もし合ってなかったら、どうぞ修正して下さい。

最後に、Cargoは「Hello, world!」を生成します。 src/main.rs を確認しましょう。

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

Cargoが用意してくれたものをコンパイルしてみましょう。

$ cargo build
   Compiling guessing_game v0.1.0 (file:///home/you/projects/guessing_game)

素晴しい!もう一度 src/main.rs を開きましょう。全てのコードをこの中に書いていきます。

行動する前に、もう一つCargoのコマンドを紹介させて下さい。 run です。 cargo runcargo build のようなものですが、生成された実行可能ファイルの実行までします。 試してみましょう。

$ cargo run
   Compiling guessing_game v0.1.0 (file:///home/you/projects/guessing_game)
     Running `target/debug/guessing_game`
Hello, world!

すごい! run コマンドはプロジェクトを細かく回す必要があるときに手頃でしょう。 今回のゲームがまさにそのようなプロジェクトです。すぐに試してから次の行動に移るの繰り返しをする必要があります。

予想値を処理する

とりかかりましょう! 数当てゲームでまずしなければいけないことはプレイヤーに予想値を入力させることです。 これを 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); }
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);
}

[訳注] それぞれの文言は

  • Guess the number!: 数字を当ててみて!
  • Please input your guess.: 予想値を入力して下さい
  • Failed to read line: 行の読み取りに失敗しました
  • You guessed: {}: あなたの予想値: {}

の意味ですが、エディタの設定などによってはソースコード中に日本語を使うと コンパイル出来ないことがあるので英文のままにしてあります。

いろいろなことあります!少しづつやっていきましょう。

fn main() { use std::io; }
use std::io;

ユーザの入力を取得して結果を出力する必要があります。 なので io 標準ライブラリからライブラリが必要になります。 Rustが全てのプログラムにデフォルトで読み込むものは少しだけで、「プレリュード」といいます。 プレリュードになければ、直接 use しなければいけません。 2つめの「プレリュード」、io プレリュードもあり、同様にそれをインポートすると io に関連した多数の有用なものがインポートされます。

fn main() {
fn main() {

以前見たように main() 関数がプログラムのエントリーポイントになります。 fn 構文で新たな関数を宣言し、 () が引数がないことを示し、 { が関数の本体部の始まりです。 返り値の型は書いていないので () 、空のタプルとして扱われます。

fn main() { println!("Guess the number!"); println!("Please input your guess."); }
    println!("Guess the number!");

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

前に println!()文字列をスクリーンに印字するマクロであることを学びました。

fn main() { let mut guess = String::new(); }
    let mut guess = String::new();

面白くなってきました!この小さな1行で色々なことが行われています。 最初に気付くのはこれが「変数束縛」を作るlet文であることです。 let文はこの形を取ります。

fn main() { let foo = bar; }
let foo = bar;

これは foo という名前の束縛を作り、それを値 bar に束縛します。 多くの言語ではこれは「変数」と呼ばれるものですが、Rustの変数束縛は少しばかり皮を被せてあります。

例えば、束縛はデフォルトでイミュータブル (不変)です。 なので、この例ではイミュータブルではなくミュータブル(可変)な束縛にするために mut を使っているのです。 let は代入の左辺に名前を取る訳ではなくて実際にはパターンを受け取ります。 後程パターンを使います。簡単なのでもう使えますね。

fn main() { // let foo = 5; // immutable. let foo = 5; // イミュータブル // let mut bar = 5; // mutable let mut bar = 5; // ミュータブル }
let foo = 5; // イミュータブル
let mut bar = 5; // ミュータブル

ああ、そして // から行末までがコメントです。Rustはコメントにある全てのものを無視します。

という訳で let mut guess がミュータブルな束縛 guess を導入することを知りました。 しかし = の反対側、 String::new() が何であるかを見る必要があります。

String は文字列型で、標準ライブラリで提供されています。 Stringは伸長可能でUTF-8でエンコードされたテキスト片です。

::new() 構文は特定の型の「関連関数」なので :: 構文を使っています。 つまり、これは String のインスタンスではなく String 自体に関連付けられているということです。 これを「スタティックメソッド」と呼ぶ言語もあります。

この関数は新たな空の String を作るので new() の名付けられています。 new() 関数はある種の新たな値を作るのによく使われる名前なので様々な型でこの関数を見るでしょう。

先に進みましょう。

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

さらに色々あります!一歩一歩進んでいきましょう。最初の行は2つの部分を持ちます。 これが最初の部分です。

fn main() { io::stdin() }
io::stdin()

プログラムの最初の行でどのように std::iouse したかを覚えていますか? それの関連関数を呼び出しているのです。 use std::io していないなら std::io::stdin() と書くことになります。

この関数はターミナルの標準入力へのハンドルを返します。詳しくはstd::io::Stdinを見て下さい。

次の部分はこのハンドルを使ってユーザからの入力を取得します。

fn main() { .read_line(&mut guess) }
.read_line(&mut guess)

ここで、ハンドルに対してread_line()メソッドを呼んでいます。 メソッドは関連関数のようなものですが、型自体ではなくインスタンスに対してだけ使えます。 read_line() に1つ引数を渡してもいます。 &mut guess です。

guess がどのように束縛されたか覚えてますか?ミュータブルであると言いました。 しかしながら read_lineString を引数に取りません。 &mut String を取るのです。 Rustには参照と呼ばれる機能があって、1つのデータに対して複数の参照を持つことが出来、コピーを減らすことが出来ます。 Rustの主要な売りの1つが参照をいかに安全に簡単に使えるかなので、参照は複雑な機能です。 しかしこのプログラムを作り終えるのに今すぐ詳細を知る必要はありません。 今のところ、 let と同じように参照はデフォルトでイミュータブルであるということだけ覚えておいて下さい。 なので &guess ではなく &mut guess と書く必要があるのです。

何故 read_line() は文字列へのミュータブルな参照を取るのでしょうか? read_line() はユーザが標準入力に打ったものを取得し、それを文字列に入れる役割を果たします。 なのでその文字列を引数として受け取り、そして入力文字列を追加するために文字列はミュータブルである必要があるのです。

しかしまだこの行について終わっていません。テキスト上では1行ですが、コードの論理行の1部でしかないのです。

fn main() { .expect("Failed to read line"); }
        .expect("Failed to read line");

メソッドを .foo() 構文で呼び出す時、改行してスペースを入れても構いません。 そうすることで長い行を分割出来ます。 こうすることだって 出来ました

fn main() { io::stdin().read_line(&mut guess).expect("failed to read line"); }
    io::stdin().read_line(&mut guess).expect("failed to read line");

ですがこれだと読み辛いです。ですので3つのメソッド呼び出しを3行に分割します。 read_line() については話しましたが expect についてはどうでしょう? さて、 read_line() がユーザの入力を &mut String に入れることには言及しました。 しかし値も返します。 この場合、標準ライブラリにある汎用のResultであり、そしてそれをサブライブラリに特殊化したバージョンの io::Result になります。

これらの Result 型の目的は、エラーハンドリング情報をエンコードすることです。 Result 型の値には、他の型と同じように、メソッドが定義されています。 今回は io::Resultexpect()メソッドが定義されていて、それが呼び出された値が成功でなければ与えたメッセージと共にpanic!します。 このような panic! はメッセージを表示してプログラムをクラッシュさせます。

この2つのメソッドを呼び出さないままにしておくと、プログラムはコンパイルしますが、警告が出ます。

$ cargo build
   Compiling guessing_game v0.1.0 (file:///home/you/projects/guessing_game)
src/main.rs:10:5: 10:39 warning: unused result which must be used,
#[warn(unused_must_use)] on by default
src/main.rs:10     io::stdin().read_line(&mut guess);
                   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Rustは値 Result を使っていないことを警告します。警告は io::Result が持つ特別なアノテーションに由来します。 Rustはエラーの可能性があるのに処理していないことを教えてくれるのです。 エラーを出さないためには実際にエラー処理を書くのが正しいやり方です。 幸運にも、問題があった時にそのままクラッシュさせたいならこの小さな2つのメソッドをそのまま使えます。 どうにかしてエラーから回復したいなら、別のことをしないといけませんが、それは将来のプロジェクトに取っておきます。

最初の例も残すところあと1行です。

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

これは入力を保持している文字列を印字します。 {} はプレースホルダで、引数として guess を渡しています。 複数の {} があれば、複数を引数を渡すことになります。

fn main() { let x = 5; let y = 10; println!("x and y: {} and {}", x, y); }
let x = 5;
let y = 10;

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

簡単簡単。

いずれにせよ、一巡り終えました。これまでのものを cargo run で実行出来ます。

$ cargo run
   Compiling guessing_game v0.1.0 (file:///home/you/projects/guessing_game)
     Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
6
You guessed: 6

よし!最初の部分は終わりました。キーボードから入力を取得して、出力し返すまで出来ました。

秘密の数を生成する

次に、秘密の数を生成する必要があります。Rustの標準ライブラリには乱数の機能がまだありません。 ですが、Rustチームはrand クレートを提供します。 「クレート」はRustのコードのパッケージです。今まで作ってきたのは実行可能な「バイナリクレート」です。 rand は「ライブラリクレート」で、他のプログラムから使われることを意図したコードが入っています。

外部のクレートを使う時にこそCargoが光ります。 rand を使う前に Cargo.toml を修正する必要があります。 Cargo.toml を開いて、この数行を末尾に追記しましょう。

[dependencies]

rand="0.3.0"

Cargo.toml[dependencies] (訳注: 依存)セクションは [package] セクションに似ています。 後続の行は次のセクションが始まるまでそのセクションに属します。 Cargoはどの外部クレートのどのバージョンに依存するのかの情報を取得するのにdependenciesセクションを使います。 今回のケースではバージョン0.3.0を指定していますが、Cargoは指定されたバージョンと互換性のあるバージョンを理解します。 Cargoはバージョン記述の標準、セマンティックバージョニングを理解します。 上記のようなそのままのバージョンは ^0.3.0 の略記で、「0.3.0と互換性のあるもの」という意味です。 正確に 0.3.0 だけを使いたいなら rand="=0.3.0" (等号が2つあることに注意して下さい)と書きます。 そして最新版を使いたいなら * を使います。また、バージョンの範囲を使うことも出来ます。 Cargoのドキュメントにさらなる詳細があります。

さて、コードは変更せずにプロジェクトをビルドしてみましょう。

$ cargo build
    Updating registry `https://github.com/rust-lang/crates.io-index`
 Downloading rand v0.3.8
 Downloading libc v0.1.6
   Compiling libc v0.1.6
   Compiling rand v0.3.8
   Compiling guessing_game v0.1.0 (file:///home/you/projects/guessing_game)

(もちろん、別のバージョンが表示される可能性もあります。)

色々新しい出力があります! 外部依存が出来たので、Cargoはそれぞれの最新版をレジストリ—Crates.ioのコピー—から取得します。 Crates.ioはRustのエコシステムに居る人が他人が使うためにオープンソースのRustプロジェクトを投稿する場所です。

レジストリをアップデートした後にCargoは [dependencies] を確認し、まだダウンロードしていないものをダウンロードします。 今回のケースでは rand に依存するとだけ書いてますが libc も取得されています。これは rand が動作するのに libc に依存するためです。 これらのダウンロードが終わったら、それらのコンパイル、そしてプロジェクトのコンパイルをします。

もう一度 cargo build を走らせると、異なった出力になります。

$ cargo build

そうです、何も出力がありません!Cargoはプロジェクトがビルドされていて、依存もビルドされていることを知っているのでそれらのことをする必要がないのです。 何もすることがなければそのまま終了します。もし src/main.rs を少し変更して保存したら、次のような行を目にするはずです。

$ cargo build
   Compiling guessing_game v0.1.0 (file:///home/you/projects/guessing_game)

Cargoには rand0.3.x を使うと伝えたので、それが書かれた時点での最新版、 v0.3.8 を取得しました。 ですが来週 v0.3.9 が出て、重要なバグフィクスがされたらどうなるのでしょう? バグフィクスを取り込むのは重要ですが、 0.3.9 にコードが動かなくなるようなリグレッションがあったらどうしましょう?

この問題への回答はプロジェクトのディレクトリにある Cargo.lock です。 プロジェクトを最初にビルドした時に、Cargoは基準を満たす全てのバージョンを探索し、 Cargo.lock ファイルに書き出します。 その後のビルドではCargoはまず Cargo.lock ファイルがあるか確認し、再度バージョンを探索することなく、そこで指定されたバージョンを使います。 これで自動的に再現性のあるビルドが手に入ります。 言い換えると、明示的にアップグレードしない限り我々は 0.3.8 を使い続けますし、ロックファイルのおかげでコードを共有する人も 0.3.8 を使い続けます。

v0.3.9 を使いたい時はどうすればいいのでしょうか? Cargoには「ロックを無視して、指定したバージョンを満たす全ての最新版を探しなさい。もし出来たらそれをロックファイルに書きなさい」を意味する別のコマンド、 update があります。 しかし、デフォルトではCargoは 0.3.0 より大きく、 0.4.0 より小さいバージョンを探しにいきます。 0.4.x より大きなバージョンを使いたいなら直接 Cargo.toml を更新する必要があります。 そうしたら、次に cargo build をする時に、Cargoはインデックスをアップデートして rand への制約を再度評価します。

Cargoそのエコシステムについては色々言うことがあるのですが今のところこれらのことだけを知っておいて下さい。 Cargoのお陰でライブラリの再利用は本当に簡単になりますし、Rustaceanは他のパッケージをいくつも使った小さなライブラリをよく書きます。

rand を実際に 使う ところに進みましょう。次のステップはこれです。

extern crate rand; 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); }
extern crate rand;

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

訳注: 先程と同じ理由でソースコード内の文言は翻訳していません。意味は

  • The secret number is: {}: 秘密の数字は: {}です

です。

まず最初に変更したのは最初の行です。 extern crate rand となっています。 rand[dependencies] に宣言したので、 extern crate でそれを使うことをRustに伝えています。 これはまた、 use rand; とするのと同じこともしますので、 rand にあるものは rand:: と前置すれば使えるようになります。

次に、もう1行 use を追加しました。 use rand::Rng です。 すぐに、とあるメソッドを使うのですが、それが動作するには Rng がスコープに入っている必要があるのです。 基本的な考え方はこうです: メソッドは「トレイト」と呼ばれるもので定義されており、メソッドが動作するにはそのトレイトがスコープにある必要があるのです。 詳しくはトレイトセクションを読んで下さい。

中ほどにもう2行足してあります。

fn main() { let secret_number = rand::thread_rng().gen_range(1, 101); println!("The secret number is: {}", secret_number); }
    let secret_number = rand::thread_rng().gen_range(1, 101);

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

rand::thread_rng() を使って現在いるスレッドにローカルな乱数生成器のコピーを取得しています。 上で use rand::Rng したので生成器は gen_range() メソッドを使えます。 このメソッドは2つの引数を取り、それらの間にある数を生成します。 下限は含みますが、上限は含まないので1から100までの数を生成するには 1101 を渡す必要があります。

2つ目の行は秘密の数字を印字します。 これは開発する時には有用で、簡単に動作確認出来ます。 しかし最終版では削除します。 最初に答えが印字されたらゲームじゃなくなってしまいます!

何度か新たなプログラムを実行してみましょう。

$ cargo run
   Compiling guessing_game v0.1.0 (file:///home/you/projects/guessing_game)
     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

良し良し。次は予想値と秘密の数字を比較します。

予想値と比較する

ユーザーの入力を受け取れるようになったので、秘密の数字と比較しましょう。 コンパイル出来ませんが、これが次のステップです。

extern crate rand; use std::io; use std::cmp::Ordering; 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); match guess.cmp(&secret_number) { Ordering::Less => println!("Too small!"), Ordering::Greater => println!("Too big!"), Ordering::Equal => println!("You win!"), } }
extern crate rand;

use std::io;
use std::cmp::Ordering;
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);

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

訳注: 同じく、

  • Too small!: 小さすぎます!
  • Too big!: 大きすぎます!
  • You win!: あなたの勝ちです!

いくか新しいことがあります。まず、新たに use が増えました。 std::cmp::Ordering をスコープに導入します。 そして、末尾にそれを使うコードが5行増えてます。

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

cmp() は比較可能なものに対しならなんでも呼べて、引数に比較したい対象の参照を取ります。 cmp() は先程 use した Ordering を返します。 match文を使って正確に Ordering のどれであるかを判断しています。 Orderingenum (訳注: 列挙型)で、enumは「enumeration(訳注: 列挙)」の略です。 このようなものです。

fn main() { enum Foo { Bar, Baz, } }
enum Foo {
    Bar,
    Baz,
}

この定義だと、 Foo 型のものは Foo::Bar あるいは Foo::Baz のいずれかです。 :: を使って enum のバリアントの名前空間を指示します。

Ordering enum は3つのバリアントを持ちます。 LessEqual そして Greater です。 match 文ではある型の値を取って、それぞれの可能な値に対する「腕」を作れます。 Ordering には3種類あるので、3つの腕を作っています。

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

Less なら Too small! を、 Greater なら Too big! を、 Equal なら You win! を印字します。 match はとても便利で、Rustでよく使われます。

これはコンパイルが通らないと言いました。試してみましょう。

$ cargo build
   Compiling guessing_game v0.1.0 (file:///home/you/projects/guessing_game)
src/main.rs:28:21: 28:35 error: mismatched types:
 expected `&collections::string::String`,
    found `&_`
(expected struct `collections::string::String`,
    found integral variable) [E0308]
src/main.rs:28     match guess.cmp(&secret_number) {
                                   ^~~~~~~~~~~~~~
error: aborting due to previous error
Could not compile `guessing_game`.

ふぅ!大きなエラーです。核心になっているのは「型の不一致」です。 Rustには強い静的な型システムがあります。しかし型推論も持っています。 let guess = String::new() と書いた時、Rustは guess が文字列である筈だと推論出来るのでわざわざ型を書かなくてもよいのです。 secret_number には、は1から100までの数字を持っている数値型、32bit数の i32 、あるいは符号なし32bit数の u32 、あるいは64bit不動小数点数 f64 あるいはそれ以外、様々な型がありえます。 これまで、それは問題ではありませんでしたので、Rustは i32 をデフォルトとしてました。 しかしながらここで、 guesssecret_number の比較の仕方が分かりません。 これらは同じ型である必要があります。 究極には入力として読み取った String を比較のために実数の型にしたいです。 それは3行追加すれば出来ます。 新しいプログラムです。

extern crate rand; use std::io; use std::cmp::Ordering; 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"); 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!"), } }
extern crate rand;

use std::io;
use std::cmp::Ordering;
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");

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

新しい3行はこれです。

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

ちょっと待って下さい、既に guess を定義してありますよね? してあります、が、Rustでは以前の guess の定義を新しいもので「隠す」ことが出来ます(訳注: このように隠すことをシャドーイングといいます)。 まさにこのように、最初 String であった guessu32 に変換したい、というような状況でよく使われます。 シャドーイングのおかげで guess_strguess のように別々の名前を考える必要はなくなり、 guess の名前を再利用出来ます。

guess を先に書いたような値に束縛します。

fn main() { guess.trim().parse() }
guess.trim().parse()

ここでは、 guess は古い guess 、入力を保持している Stringguess です。 Stringtrim() メソッドは文字列の最初と最後にある空白を取り除きます。 read_line() を満たすには「リターン」キーを押す必要があるのでこれは重要です。 つまり、 5 と入力してリターンを押したら、 guess5\n のようになっています。 \n 「は改行」、エンターキーを表しています。 trim()5 だけを残してこれを取り除けます。 文字列の parse() メソッドは文字列を何かの数値へとパースします。 様々な数値をパース出来るので、Rustに正確にどの型の数値が欲しいのかを伝える必要があります。 なので、 let guess: u32 なのです。 guess の後のコロン(:)は型注釈を付けようとしていることをRustに伝えます。 u32 は符号なし32bit整数です。 Rustには様々なビルトインの数値型がありますが、今回は u32 を選びました。 小さな正整数にはちょうどいいデフォルトの選択肢です。

read_line() と同じように、 parse() の呼び出しでもエラーが起き得ます。 文字列に A👍% が含まれていたらどうなるでしょう?それは数値には変換出来ません。 なので、 read_line() と同じように expect() を使ってエラーがあったらクラッシュするようにします。

プログラムを試してみましょう。

$ cargo run
   Compiling guessing_game v0.1.0 (file:///home/you/projects/guessing_game)
     Running `target/guessing_game`
Guess the number!
The secret number is: 58
Please input your guess.
  76
You guessed: 76
Too big!

よし!予想値の前にスペースも入れてみましたがそれでもちゃんと76と予想したんだと理解してくれます。 何度か動かしてみて、当たりが動くこと、小さい数字も動くことを確認してみて下さい。

ゲームのほとんどが完成するようになりましたが、1回しか予想出来ません。 ループを使って書き換えましょう!

ループ

loop キーワードで無限ループが出来ます。入れてみましょう。

extern crate rand; use std::io; use std::cmp::Ordering; 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 { 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); match guess.cmp(&secret_number) { Ordering::Less => println!("Too small!"), Ordering::Greater => println!("Too big!"), Ordering::Equal => println!("You win!"), } } }
extern crate rand;

use std::io;
use std::cmp::Ordering;
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 {
        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);

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

そして試してみましょう。でも待って下さい、無限ループを追加しませんでした? そうです。 parse() に関する議論を覚えてますか?数字でない答えを入力すると panic! して終了するのでした。 やってみましょう。

$ cargo run
   Compiling guessing_game v0.1.0 (file:///home/you/projects/guessing_game)
     Running `target/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!'

おっ! quit で確かに終了しました。他の数字でないものでも同じことです。 でもこれは控え目に言っても最適とは言えません。 まず、ゲームに勝ったら本当に終了するようにしましょう。

extern crate rand; use std::io; use std::cmp::Ordering; 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 { 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); match guess.cmp(&secret_number) { Ordering::Less => println!("Too small!"), Ordering::Greater => println!("Too big!"), Ordering::Equal => { println!("You win!"); break; } } } }
extern crate rand;

use std::io;
use std::cmp::Ordering;
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 {
        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);

        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() の最後の要素なので、プログラムが終了することも意味します。 もう1つ調整をします。数値でない入力をした時に、終了したくはありません、無視したいです。 それはこのように出来ます。

extern crate rand; use std::io; use std::cmp::Ordering; 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 { 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; } } } }
extern crate rand;

use std::io;
use std::cmp::Ordering;
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 {
        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;
            }
        }
    }
}

変更はこれです。

fn main() { let guess: u32 = match guess.trim().parse() { Ok(num) => num, Err(_) => continue, }; }
let guess: u32 = match guess.trim().parse() {
    Ok(num) => num,
    Err(_) => continue,
};

これが「エラーならクラッシュ」から「実際に返値のエラーをハンドルする」への一般的な移行の仕方です。 parse() の返す値は Ordering と同じような enum ですが、今回はそれぞれのバリアントにデータが関連付いています。 Ok は成功で、 Err は失敗です。それぞれには追加の情報もあります。パースに成功した整数、あるいはエラーの種類です。 このケースでは、 Ok(num) に対して match していて、それで Ok に内包された値を num という名前に設定しており、右側でそのまま返しています。 Err の場合、エラーの種類は気にしにないので、名前ではなく _ を使います。 これはエラーを無視していて、 continueloop の次の繰り返しに進みます。

これで良いはずです。試しましょう!

$ cargo run
   Compiling guessing_game v0.1.0 (file:///home/you/projects/guessing_game)
     Running `target/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!

すごい!最後のほんのちょっとだけ修正して、数当てゲームを終えましょう。 なんだか分かりますか?そうです、秘密の数字は印字したくありません。 テストには良かったのですがゲームを台無しにしてしまいます。 これが最終ソースです。

extern crate rand; use std::io; use std::cmp::Ordering; use rand::Rng; 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; } } } }
extern crate rand;

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

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

完成!

ここにて数当てゲームを作ることが出来ました!おめでとうございます!

この最初のプロジェクトで色々なものを見せました。 letmatch 、メソッド、関連関数、外部クレートの使い方、などなど。 次のプロジェクトではさらに色々見せます。