今回学ぶのはmatch、シャドーイング、型変換

連載で前回取り上げたソースコードは次のとおり。このソースコードを通じてRustの提供している「標準ライブラリ」「変数」「型の関数」「標準入力」「参照」「パニック処理」「クレート」「トレイト」について学んだ。

前回まで読んできたソースコード

use std::io;
use rand::Rng;

fn main() {
    println!("数当てゲーム");

    let secret_number = rand::thread_rng().gen_range(1, 101);

    println!("シークレットナンバーはこれ: {}", secret_number);

    println!("どの数だとおもう? = ");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("読み込み失敗");

    println!("入力値: {}", guess);
}

Rustは「マルチパラダイムプログラミング言語」と分類されることがある。手続き型、オブジェクト指向、関数型など複数の特徴的なプログラミング言語の概念が取り込まれているためだ。短いソースコードから多くのことが学べるのは、Rustがマルチパラダイムプログラミング言語であり、さまざまな特徴を持っていることに1つの理由がある。

今回はザ・ブックを読み進めつつ、「matchフロー制御演算子」と「型変換」と「シャドーイング」について学んでいく。特にmatchフロー制御演算子はRust特有の書き方なので、他のメジャーなプログラミング言語のユーザには目新しいのではないかと思う。

matchフロー制御演算子

まず、数当てゲームのソースコードを次のように書き換えてみよう。

matchフロー制御演算子を加えた数当てゲーム

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

fn main() {
    println!("数当てゲーム");

    let secret_number = rand::thread_rng().gen_range(1, 101);

    println!("シークレットナンバーはこれ: {}", secret_number);

    println!("どの数だとおもう? = ");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("読み込み失敗");

    println!("入力値: {}", guess);

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("小さすぎです!"),
        Ordering::Greater => println!("大きすぎです!"),
        Ordering::Equal => println!("そのとおり!"),
    }
}

useステートメントに新しく次の行が追加されている。

新しく追加されたuseステートメント

use std::cmp::Ordering;

useステートメントでstd::cmp::Orderingをスコープに追加したのは、この型を数当てゲームで使うためだ。Orderingは列挙型で、構成子はLess、Greator、Equalだ。rust/blob/master/library/core/src/cmp.rs付近に定義があると思うので、この2つしか用意されていないことがわかるだろう(ソースコードはパスが頻繁に変わるので、別のURLになっている可能性もある。その場合はpub enum Orderingで検索をかけて該当するソースコードを発見してみてほしい)。

Ordering型の定義部分

pub enum Ordering {
    /// An ordering where a compared value is less than another.
    #[stable(feature = "rust1", since = "1.0.0")]
    Less = -1,
    /// An ordering where a compared value is equal to another.
    #[stable(feature = "rust1", since = "1.0.0")]
    Equal = 0,
    /// An ordering where a compared value is greater than another.
    #[stable(feature = "rust1", since = "1.0.0")]
    Greater = 1,
}
  • https://github.com/rust-lang/rust/blob/master/library/core/src/cmp.rs

    https://github.com/rust-lang/rust/blob/master/library/core/src/cmp.rs

matchフロー制御演算子は、他のメジャーなプログラミング言語におけるswitch制御構文やcase制御構文のようなものと考えるとわかりやすい。主な違いはその強力さというか対応範囲の広さといったところだろうか。 

先程のソースコードでは次のmatchフロー制御演算子を追加した。

新しく追加したmatchフロー制御演算子を使った部分

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("小さすぎです!"),
        Ordering::Greater => println!("大きすぎです!"),
        Ordering::Equal => println!("そのとおり!"),
    }

guess.cmp()の部分で比較を行っているわけだが、この関数はOrdering型を返り値としている。matchでその値に応じて実行する処理を切り分けているわけだ。switch制御構文やcase制御構文のようなものと考えれば飲み込みやすいだろう。if elseを列挙していくよりもわかりやすいと思う。matchフロー制御演算子は強力な機能だ。詳しい使い方はザ・ブックの別の章で出てくるのでそちらで理解を深めるとしよう。

型変換

先程のソースコードをビルドすると、次のようなエラーが出る。

この状態ではビルドできない

% cargo build
   Compiling guessing_game v0.1.0 (/Users/daichi/Documents/rust_testbed/guessing_game)
error[E0308]: mismatched types
  → src/main.rs:22:21
      match guess.cmp(&secret_number) {
                        ^^^^^^^^^^^^^^ expected struct `std::string::String`, found integer
   = note: expected reference `&std::string::String`
              found reference `&{integer}`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.
error: could not compile `guessing_game`.

To learn more, run the command again with --verbose.
% 

変数guessは文字列だ。このため、guess.cmp()での比較対象も文字列が求められる。しかし、実際には整数が指定されており、ビルドエラーになっているわけだ。

このエラーを回避するには、guessを文字列ではなく整数として用意すればよい。次のソースコードがビルドできるソースコードとなる。

ビルドできるように書き換えたソースコード

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

fn main() {
    println!("数当てゲーム");

    let secret_number = rand::thread_rng().gen_range(1, 101);

    println!("シークレットナンバーはこれ: {}", secret_number);

    println!("どの数だとおもう? = ");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("読み込み失敗");

    let guess: u32 = guess
        .trim()
        .parse()
        .expect("数を入力してください!");

    println!("入力値: {}", guess);

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("小さすぎです!"),
        Ordering::Greater => println!("大きすぎです!"),
        Ordering::Equal => println!("そのとおり!"),
    }
}

ビルドできるように追加されたのは次の部分だ。

    let guess: u32 = guess
        .trim()
        .parse()
        .expect("数を入力してください!");

まず、trim()でホワイトスペースを削除している。RustではUnicodeでWhite_Spaceプロパティを持つ文字を削除する関数としてtrim()が用意されている。空白系、タブ系、改行系の文字がこれに相当する。サンプルコードでは io::stdin().read_line()で標準入力から文字列を取得しており、文字列の最後に改行コードが入っている。trim()はこれを削除するために使われている。

次にparse()でu32型の整数へ変換している。変換できない文字列が入力されていた場合にはexpect()で指定したメッセージが出力されエラーとなる。

Rustは型の扱いが静的だ。型の不整合はビルドの段階で検出できる。自動的にキャストするタイプのプログラミング言語は便利ではあるが、問題の発生に気が付きにくいことがある。静的だとコーディングが増えるが、ビルド時に型不整合を検出でき、事前に問題をつぶしやすくなる。

シャドーイング

さて、先程のサンプルソースコードで不可解なことに気がついた方もいるのではないだろうか。まず、guessという変数が定義されている。これはこれまで通りだ。

文字列の変数guess

    let mut guess = String::new();

標準入力から文字列データを得たあとで、次のようにもう一度変数guessが作成されている。ビルドエラーになりそうなものだが、これはエラーにはならない。

整数の変数guess

    let guess: u32 = guess
        .trim()
        .parse()
        .expect("数を入力してください!");

Rustでは、このように同じ変数名の変数をletで作ることを「シャドーイング」と呼んでいる。シャドーイングは次の目的で使われることが多い。

  • むやみに変数名を増やさせない
  • 変数の型を変える

例えば、同じことを他のプログラミング言語でやろうとした場合、文字列ならguess_str、整数ならguess_intといったように別の名前の変数を用意するのではないだろうか。Rustのシャドーイングであれば、名前は同じまま型を変えることができる。変数を増加させずに型を変換する、これはRustの特徴的な使い方だ。

可変変数なら中身を変更することはできるが、型は変えることができない。かといって、曖昧な型を用意するようなアプローチを取ればビルド時の徹底したチェックが難しくなる。シャドーイングはプログラミング言語としての堅さを保ったまま使いやすさも担保する仕組みといえる。

数当てゲーム

ザ・ブックの数当てゲームもかなり完成に近づいてきた。現在のソースコードは次のようにビルドできるはずだ。

ビルド可能になった数当てゲーム

% cargo build
   Compiling guessing_game v0.1.0 (/Users/daichi/Documents/rust_testbed/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 0.30s
%

次にそれぞれの実行サンプルを掲載する。それぞれ目的通りに動作していることを確認できる。

数当てゲーム実行例:ビンゴ

% cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/guessing_game`
数当てゲーム
シークレットナンバーはこれ: 46
どの数だとおもう? = 
46
入力値: 46
そのとおり!
% 

数当てゲーム実行例:値が大きい

% cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/guessing_game`
数当てゲーム
シークレットナンバーはこれ: 58
どの数だとおもう? = 
100
入力値: 100
大きすぎです!
% 

数当てゲーム実行例:値が小さい

% cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/guessing_game`
数当てゲーム
シークレットナンバーはこれ: 45
どの数だとおもう? = 
1
入力値: 1
小さすぎです!
% 

数当てゲーム実行例:数字以外を入力したケース

% cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/guessing_game`
数当てゲーム
シークレットナンバーはこれ: 4
どの数だとおもう? = 
a
thread 'main' panicked at '数を入力してください!: ParseIntError { kind: InvalidDigit }', src/main.rs:20:22
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
% 

短いながらも多くのことを教えてくれる数当てゲームも完成が近づいてきた。これだけのソースコードなのにそこから読み取れる機能やバックエンドはとても大きい。この辺りがRustの面白さであり難しさでもある。

しかし、プログラミングの経験があるなら、既に「これは堅いが、よい感じで現実に落とし込まれている」といった雰囲気を感じるのではないかと思う。もうちょっとだけ数当てゲームを追っていこう。

参考資料