数当てゲーム

Rustプロジェクト公式の入門書「The Rust Programming Language - The Rust Programming Language」では、まず「数当てゲーム」のサンプルソースコードを説明する形でプログラミング言語「Rust」の説明を行っている。数当てゲームはプログラミングの導入でよく使われるプログラムの1つだ。

数当てゲームでは、プログラムはランダムにある数字を生成する。ユーザーがその数字を当てるというゲームだ。ユーザーはまず当てずっぽうに数を指定する。プログラムはその数が、生成した数と同じか、その数よりも大きいか、その数よりも小さいかを出力する。ユーザーはその結果を見て、次の数を適当に入力する。繰り返していけばどんどん範囲を狭めていって、プログラムが生成した数を特定することができるという仕組みになっている。

このプログラムは短く、加えて次のような要素を含んでいる。

  • 分岐処理
  • 繰り返し処理
  • ユーザーから入力を受け付ける処理
  • ユーザーにメッセージを出力する処理

短いサンプルコードである上、プログラミングする上で基本となるさまざまな要素を含んでいる。プログラミング入門時の教材としては都合がよいのだ。ザ・ブックも数当てゲームをサンプルに取り上げており、ここでもその流れでRustを学習していこうと思う。

今回は、ザ・ブックに掲載されているサンプルコードから最初に理解しておきたい部分をかいつまんで説明する。ザ・ブックでは、ライブラリ、標準ライブラリ、変数、型、関数、コメント、フロー制御、オーナーシップ、参照と借用など、Rustの特徴的な機能は数当てゲームのあとに詳しく説明しているので、今回は概要程度にとどめておく。

サンプルコードとビルド&実行

まずサンプルコードを用意して実行してみよう。まず、次のようにして数当てゲーム(guessing_game)のソースコードを用意する。

数当てゲームのセットアップ

cargo new guessing_game

main.rsの中身を次のようなソースコードにする。ザ・ブックに掲載されているソースコードのメッセージだけを日本語に置き換えたものだ。

main.rs

use std::io;

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

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

    let mut guess = String::new();

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

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

次のようにビルドする。

ビルド

cargo build

実行すると次のようになる。ここではユーザは50という数字を入力している。

実行

% cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/guessing_game`
数当てゲーム
どの数だとおもう? = 
50
入力値: 50

% 
  • 数当てゲーム実行サンプル

    数当てゲーム実行サンプル

ザ・ブックに最初に掲載されているサンプルコードは数当てゲームの途中までしか実装していない。しかし、これだけでもRustの多くのことを語っている。Rustは愛されやすいプログラミング言語だが、決して簡単なプログラミング言語ではないのだ。このサンプルだけで多くの概念を理解する必要がある。

標準ライブラリ

ソースコードの読み出しとしては、まずRustの標準ライブラリとその使い方を理解する必要があると思う。標準ライブラリはプログラミング言語として基本的な実装をまとめたものが該当することが多い。Rustでは最も基本となる型がこの「標準ライブラリ」にまとめられている。なお、この標準ライブラリというものはデフォルトで使えるのかといえば、デフォルトではその一部しか使えない(使えないというかスコープに入っていないというか)。

Rustで標準ライブラリはstdとして表現される。標準ライブラリは必要性の高い型がまとまっているが、そのすべてが必要ということはない。Rustはデフォルトで使用する型は必要最小限であるべきと考えており、可能な限り小さくしている。その必要最小限な型は標準ライブラリに含められているpreludeと呼ばれるライブラリにまとめられており、本稿執筆時点では「std::prelude::v1::*」がこれに該当する。

Rustではuseステートメーンとでライブラリをスコープに追加する(必須ではないのだが、ライブラリを使う時はuseで使うライブラリを指定すると考えておけばよいと思う。これを使わないとコードが冗長になりすぎて見にくくなる)。つまり、次のuseステートメントがRustによって自動的に挿入されていると考えておけばよい。

useステートメントをこう使っているのと同じ

use std::prelude::v1::*;

std::prelude::v1::*には次のような型が含められている。ここに記載されている型はデフォルトで使用できるということだ。

std::prelude::v1でエクスポートされている型

pub use crate::marker::Send;
pub use crate::marker::Sized;
pub use crate::marker::Sync;
pub use crate::marker::Unpin;
pub use crate::ops::Drop;
pub use crate::ops::Fn;
pub use crate::ops::FnMut;
pub use crate::ops::FnOnce;
pub use crate::mem::drop;
pub use crate::convert::AsMut;
pub use crate::convert::AsRef;
pub use crate::convert::From;
pub use crate::convert::Into;
pub use crate::iter::DoubleEndedIterator;
pub use crate::iter::ExactSizeIterator;
pub use crate::iter::Extend;
pub use crate::iter::IntoIterator;
pub use crate::iter::Iterator;
pub use crate::option::Option;
pub use crate::option::Option::None;
pub use crate::option::Option::Some;
pub use crate::result::Result;
pub use crate::result::Result::Err;
pub use crate::result::Result::Ok;
pub use core::prelude::v1::asm;
pub use core::prelude::v1::assert;
pub use core::prelude::v1::cfg;
pub use core::prelude::v1::column;
pub use core::prelude::v1::compile_error;
pub use core::prelude::v1::concat;
pub use core::prelude::v1::concat_idents;
pub use core::prelude::v1::env;
pub use core::prelude::v1::file;
pub use core::prelude::v1::format_args;
pub use core::prelude::v1::format_args_nl;
pub use core::prelude::v1::global_asm;
pub use core::prelude::v1::include;
pub use core::prelude::v1::include_bytes;
pub use core::prelude::v1::include_str;
pub use core::prelude::v1::line;
pub use core::prelude::v1::llvm_asm;
pub use core::prelude::v1::log_syntax;
pub use core::prelude::v1::module_path;
pub use core::prelude::v1::option_env;
pub use core::prelude::v1::stringify;
pub use core::prelude::v1::trace_macros;
pub use crate::borrow::ToOwned;
pub use crate::boxed::Box;
pub use crate::string::String;
pub use crate::string::ToString;
pub use crate::vec::Vec;

数当てゲームはユーザーから数の入力をしてもらいその値を得る必要があり、さらにユーザーにメッセージを読んでもらう必要がある。このような機能は入出力ライブラリ(ioライブラリ)にまとめられており、指定方法は次のようになる(入出力ライブラリは標準ライブラリに含められているのでstd::ioでアクセスする)。

入出力ライブラリをスコープに追加

use std::io;

useステートメントは、指定したパスをローカルの名前として使えるようにする機能を担っている。通常、利用するライブラリはuseで宣言して使う。useでライブラリを宣言しなくても使えるのだが、コードが長くなって見にくいので通常はuseを使う。

Rustで使われているuseというステートメントは、PerlやPHP、Fortranなんかでも同じ名前で似たような用途で使われている。これら言語に慣れている方はuseの意味が体感としてわかるかもしれない。ほかのプログラミング言語でも似たような機能があるが、使われている言葉がimportやusing、includeだったりする。また、一見似ているが概念や動作が結構違っている用語もあるので、まったく同じものと考えていると混乱するかもしれない。今は「似たような機能がある」くらいに考えておけばよいと思う。

変数

Rustでは次のようにletステートメントで変数を宣言する。

letステートメントで変数を宣言

let 変数名 = 値;

Rustの変数は特徴的だ。挙動をメモリセーフに振るというRustの設計思想がよく現れている。Rustでは変数は不変なのである。上記のように宣言した変数は後から変更することができない。ほかのプログラミング言語では、変数は基本的には可変であることが多い。あとから変更可能だ。変更されては困るものは宣言時にあえて「不変」だと指定する。これがRustの場合はデフォルトと不変になっている。

Rustでは次のようにlet mutで変数を宣言すると、可変になる。mutというのはmutable(可変)という単語の頭3文字だ。変更可能な変数を作ろうと思ったら、このようにmutをつけないといけない。

let mutで可変な変数を宣言

let mut 変数名 = 値;

Rustは一事が万事この設計思想になっているというか、問題となりやすい挙動はデフォルトで排除されている。設計思想の根底にあるこの「なぜ」を知っておかないと、コーディングしていてかなりイラつくと思う。こんな簡単なことをするのになぜわざわざこんな面倒くさい書き方をしなければいけないのか、と。しかし、結局それに身を救われることになるので、最後にはRustを好きになってしまう。

型に関連付けられた関数

数当てゲームでは、次のようにguessという変数が作成されている。

数当てゲームの変数

let mut guess = String::new();

先程の説明からわかるように、guessは可変な変数だ。そして、Stringという型の変数であることもわかる。std::prelude::v1::*には次のようにStringが含まれており、この型を使っていることになる。

Stringはstd::prelude::v1::*に含まれておりデフォルトで使用可能

pub use crate::string::String;

ここでは次の記述にも注目する必要がある。

型に関連付けられた関数new()を呼んでいる

String::new()

この記述はStringに関連付けられているnew()という関数を呼ぶという処理になっている。Stringは後から文字を増やすことができるUTF-8エンコードの文字列を表しており、new()を呼び出すと空の文字列を生成して返してくれる。new()という関数はほかの型でも同じような用途で実装されていることが多い。

このString::new()はオブジェクト指向のプログラミング言語ではスタティックメソッドと呼ばれるものに近く、プログラミング言語によってはクラスメソッドと呼ばれるものが近い。

ザ・ブックの説明だけでは実態が想像できないことも多いと思うのだが、そんな時はソースコードを直接読んでしまうとよい。例えば、Stringの実装はrust/src/liballoc/string.rsだ。このファイルの中身を、Stringとnew()に関連するところだけざっくり抜き出すと、次のようになる。

use core::char::{decode_utf16, REPLACEMENT_CHARACTER};
use core::fmt;
use core::hash;
use core::iter::{FromIterator, FusedIterator};
use core::ops::Bound::{Excluded, Included, Unbounded};
use core::ops::{self, Add, AddAssign, Index, IndexMut, RangeBounds};
use core::ptr;
use core::str::{lossy, pattern::Pattern};

use crate::borrow::{Cow, ToOwned};
use crate::boxed::Box;
use crate::collections::TryReserveError;
use crate::str::{self, from_boxed_utf8_unchecked, Chars, FromStr, Utf8Error};
use crate::vec::Vec;

pub struct String {
    vec: Vec<u8>,
}

impl String {
    pub const fn new() -> String {
        String { vec: Vec::new() }
    }

    pub fn with_capacity(capacity: usize) -> String 
    pub fn from_str(_: &str) -> String
    pub fn from_utf8(vec: Vec<u8>) -> Result<String, FromUtf8Error>
    pub fn from_utf8_lossy(v: &[u8]) -> Cow<'_, str>
    pub fn from_utf16(v: &[u16]) -> Result<String, FromUtf16Error>
    pub fn from_utf16_lossy(v: &[u16]) -> String
    pub fn into_raw_parts(self) -> (*mut u8, usize, usize)
    pub fn into_bytes(self) -> Vec<u8>
    pub fn as_str(&self) -> &str
    pub fn as_mut_str(&mut self) -> &mut str
    pub fn push_str(&mut self, string: &str)
    pub fn capacity(&self) -> usize 
    pub fn reserve(&mut self, additional: usize)
    pub fn reserve_exact(&mut self, additional: usize)
    pub fn try_reserve(&mut self, additional: usize) -> Result<(), TryReserveError>
    pub fn try_reserve_exact(&mut self, additional: usize) -> Result<(), TryReserveError>
    pub fn shrink_to_fit(&mut self)
    pub fn shrink_to(&mut self, min_capacity: usize)
    pub fn push(&mut self, ch: char) 
    pub fn as_bytes(&self) -> &[u8]
    pub fn truncate(&mut self, new_len: usize)
    pub fn pop(&mut self) -> Option<char>
    pub fn remove(&mut self, idx: usize) -> char
    pub fn retain<F>(&mut self, mut f: F)
    pub fn insert(&mut self, idx: usize, ch: char)
    pub fn insert_str(&mut self, idx: usize, string: &str)
    pub fn len(&self) -> usize
    pub fn is_empty(&self) -> bool
    pub fn split_off(&mut self, at: usize) -> String
    pub fn clear(&mut self)
    pub fn drain<R>(&mut self, range: R) -> Drain<'_>
    pub fn replace_range<R>(&mut self, range: R, replace_with: &str)
    pub fn into_boxed_str(self) -> Box<str>
}

Stringは「pub strust String」として宣言され、実際には「vec: Vec<u8>」であること、new()はStringの実装で「pub const fn new() -> String」と宣言され、実際には「vec: Vec::new()」が呼ばれていることなどがわかる。

こうやって構造と実装を調べてみると、ザ・ブックで型(type)と呼ばれているものや、String型に関連付けられた関数(an associated function of the String type)、という説明がどういったものであるかよくわかる。

一歩一歩読み解いていく

数当てゲームの最初の数行だけを見ただけだが、すでにスコープ、名前空間、ライブラリ、関数、変数(可変、不変)、型、型に結び付けられた関数、といった概念が使われている。オブジェクト指向プログラミング言語の経験がある方ならそれほど難しい話ではないが、スクリプト系の言語を簡単な用途でしか使っていないと、なぜこの程度のことでこんなにいろいろ概念が出てくるのか、不思議に思うかもしれない。

Pythonなど人気の高いスクリプト言語はあまり概念を理解しなくてもある程度使える仕組みになっているものの、ちゃんと動作を理解しようと思ったら、数行程度でもかなり多くの概念を理解しておく必要がある。その点で言えば、Rustもそれ以外の言語もそれほど難しさは変わらないかもしれない。

ただ、Rustの場合、ちゃんと理解していないとまずコンパイルを通るコードを書くことができない点で、スクリプト系のプログラミング言語とは大きく異なっている。しかし、Rustで得た概念はほかのプログラミング言語を使う時の思考整理やコーディング整理にも応用できるので、やっておいて損はないと思う。しばらくは新しい概念の話が続くかもしれないが、一歩一歩読み解いていこう。

参考資料