実践的なアプリケーションを書いてみよう! Rustの構造化プログラミング【第二言語としてのRust】

Rustを使って、実際にアプリケーションを3つ書いてみましょう! その前に、プログラムの構造化に必要な手法、ジェネリクス、構造体、列挙型、それにトレイトといった概念についても説明します。

実践的なアプリケーションを書いてみよう! Rustの構造化プログラミング【第二言語としてのRust】

前回の記事では、Rustの基本的な文法や型を説明し、他のプログラミング言語ではあまり見かけない、Rustならではのメモリ管理の仕組み(所有権・参照・ライフタイム)についても解説しました。

今回は、Rustを使って、実際にアプリケーションを書いてみましょう。

制作するのは、Unix環境ではお馴染みの文字列検索プログラムであるgrep(その機能限定版をRustで書いたrsgrepアナグラム(単語の文字を入れ替えた単語)を求めるプログラム、そしてHTTP/0.9サーバです。

しかし、アプリケーション開発に入る前に、もう少しだけ説明しておくことがあります。それは、プログラムの構造化に必要な手法です。ある程度の規模のプログラムをRustで書いていくには、前回説明した基礎に加え、ジェネリクス、構造体・列挙型、それにトレイトといった概念を知っておく必要があります。

今回はまず、単純なサンプルコードを例に、これらの概念について説明しましょう。

ジェネリクス

ジェネリクスは、関数やデータ型を任意の型に対して動作するように一般化するときに使える仕組みです。ジェネリクスを利用することで、何度も同じような関数やデータ型の定義をしなくて済みます。

関数については、次のように定義することで、ジェネリックな型を持つ関数を定義できます。

fn 関数名<型パラメータ, ...>(引数) -> 返り値の型 {
    関数本体
}

型パラメータとして指定したものは、引数返り値の両方で使えます。

例として、任意の型の引数を2つ取り、それらのタプルを返す関数を定義して使ってみましょう。

fn pair<T, S>(t: T, s: S) -> (T, S) { (t, s) }

fn main() {
    // T = i32, S = f64で呼び出す
    let i = pair(1, 1.0);

    // 型を明示する方法もある
    let i = pair::<isize, f64>(1, 1.0);

    // T = &str, S = Stringで呼び出す
    let s = pair("str", "string".to_string());
}

関数だけでなく、これから説明する構造体や列挙型を含むさまざまなデータ型も、同様の構文でジェネリックにできます。後半の実践例でも、そのようなジェネリクスの例が登場する予定です。

構造体

Rustでは、複数のデータ型を集めて構造体を定義できます。

Rustの構造体は、オブジェクト指向プログラミングの際によく使われ、Javaのような言語におけるクラスと同じような使い方ができます。

ただし、Rustの構造体には継承がありません。これにはさまざまな理由(技術的トレードオフ、エルゴノミクス的トレードオフなど)があるのですが、後述するトレイトが強力なので継承が必要ないという面もあるでしょう。

Rustで構造体を定義するにはstructを使います。structには、データを持たないUnit構造体を定義する場合、フィールドに名前がないタプル構造体を定義する場合、それ以外の通常の構造体を定義する場合の3種類の構文があります。

これら3種類の構造体をすべて使った例を下記に用意しました。

// struct 名前; (Unit構造体の構文)
struct Dummy;

// struct 名前(型, ..); (タプル構造体の構文)
struct Point(f64, f64);

// struct 名前 {フィールド: 型, ..} (通常の構造体の構文)
struct Color {
    r: u8,
    g: u8,
    // 最後のフィールドの末尾にもカンマを付けられる
    b: u8,
}

fn main() {
    // Unit構造体は名前でそのまま初期化
    let dummy = Dummy;

    // タプル構造体は関数のように初期化
    // 実際、関数として扱うこともできる
    let point = Point(0.0, 0.0);

    // タプル構造体のフィールドへのアクセス
    let x = point.0;

    // 普通の構造体の初期化
    let black = Color { r: 0, g: 0, b: 0};

    // 普通の構造体のフィールドへのアクセス
    let r = black.r;
}

Unit構造体は、あまり馴染みがないかもしれませんが、後述のimplやトレイトでよく使います。

タプル構造体は、フィールドが1、2個の構造体を使うときに用いることが多いようです。フィールドに名前がありませんが、タプルと同じように値.インデックスとすることでフィールドにアクセスできます。

構造体の実装(impl

構造体の名前にimplをつけることで、メソッドや関連関数(クラスメソッドのようなもの)を定義できます。

下記は、絶対温度と摂氏を変換するプログラムです。それぞれをKelvinおよびCelsiusというタプル構造体として定義し、Celsiusに対してKelvinとの変換をする関数をimplで実装しています。

struct Celsius(f64);
struct Kelvin(f64);

// `impl 型名 {..}`で型に対する実装を書ける
impl Celsius {
    // `{..}`の中には関数が書ける。
    // 第一引数が`self`、`&mut self` `&self`, `Box<self>`の場合はメソッドとなる
    fn to_kelvin(self) -> Kelvin {
        // selfを通じてフィールドにアクセスできる。
        Kelvin(self.0 + 273.15)
    }

    // 第一引数が`self`系でない場合は関連関数となる
    fn from_kelvin(k: Kelvin) -> Self {
        Celsius(k.0 - 273.15)
    }
}

fn main() {
    let absolute_zero = Kelvin(0.0);
    let triple_point = Celsius(0.0);
    // 関連関数は`型名::関数名(引数)`で呼び出す。
    let celsius = Celsius::from_kelvin(absolute_zero);
    // メソッドは`値.関数名(引数)`で呼び出す。
    let kelvin = triple_point.to_kelvin();
}

列挙型

構造体と並んでRustでよく使われるのが、列挙型です。

列挙型は、列挙子の「どれか1つ」を表す型です。このような型は関数型言語では古くから使われていて、「代数的データ型」「直和型」「タグ付きUnion」などの呼び方もあります。関数型言語での実績が認められ、ようやくRustのような言語でも採用されるようになってきたといったところでしょうか。

列挙型を定義するには、enumを使い、enum 名前 {列挙子, ..}のようにします。列挙子には、構造体の3種類の構文と同じ要領で、3種類の定義方法があります。

例を見てみましょう。下記では、RightUpMovePrintという列挙子を持つCommandという名前の列挙型を定義しています。指定した方向に進んだり座標に移動したりして何かをするコマンドを定義しているイメージです。

enum Command {
    // 列挙子は構造体のように3種類の定義ができる
    Right(i64),
    Up(i64),
    Move { x: i64, y: i64 },
    Print,
}

列挙子へのアクセスには、列挙型名::列挙子名という構文を使います。

上記で定義した列挙型を使って、コマンドを登録して実行するプログラムを書いてみましょう。この例のように、列挙型を利用するときはmatch式と組み合わせることがよくあります。

fn main() {
    let mut cur = (0, 0);
    // 列挙子を指定してコマンドを登録
    let commands = &[Command::Move { x: 0, y: 0 },
                     Command::Right(5),
                     Command::Up(5),
                     Command::Print,
                     Command::Move { x: 10, y: 10 },
                     Command::Print];
    for c in commands {
        // match式で値を取り出す
        match *c {
            // match式でのパターンマッチでも、列挙型名を明記する
            Command::Right(x) => cur.0 += x,
            Command::Up(y) => cur.1 += y,
            // フィールド名がある列挙子のパターンマッチ
            Command::Move { x, y } => {
                cur.0 = x;
                cur.1 = y;
            }
            Command::Print => {
                println!("{:?}", cur);
            }
        }
    } // => (5, 5)
      //    (10, 10)
}

Option型とResult型

ここで、Rustのプログラミングでよく使う列挙型を2つ紹介しておきましょう。Option型とResult型です。どちらも標準ライブラリで定義されています。

// 構造体や列挙型、トレイトにもジェネリクスはある。
enum Option<T> {
    // 値がないか
    None,
    // ある
    Some(T),
}

enum Result<T, E> {
    // 計算が成功したか
    Ok(T),
    // 失敗してエラーが出たか
    Err(E),
}

Rustには例外というものがなく、エラーも返り値で表します。そのときに活躍するのがこれらの型です。

Option型は、ハッシュマップからの値の取得など「値があれば返すが見つからなければnil」という処理をするときに使われます。Result型は、計算が失敗するかもしれないときに使われる型です。なお、Result型については特別な構文糖衣もあります。

トレイト

トレイトは、Rustでポリモーフィズムを実現する手段の1つです。他の言語にも、Rustのトレイトと似た機能は、インターフェースやモジュールといった名前で用意されています。一番近いのは、Haskellなどにある型クラスでしょうか。

Rustでは、トレイトのおかげで、無関係な型同士で共通する振る舞いを作ったり、(プリミティブを含む)既存の型を拡張できたりといった、素晴らしい抽象化が可能です。私はJavaのインターフェースがクラスの定義時にしか実装できないことに不満があったのですが、トレイトではその問題も見事に解決されます。

トレイトは、「あるメソッドを実装している型」を表すのにも向いています。静的型付き言語で合法的にダックタイピングができるようになる楽しい機能だといえるかもしれません。その意味では、ダックタイピングに馴染んでいる人にとって、しっくりくる機能だといえるでしょう。

実際にトレイトを使ったプログラムを見てみましょう。下記のサンプルプログラムでは、DuckLikeというトレイトを定義しています。DuckLikeを実装するデータ型には、鳴き方を表すquackメソッドと、歩き方を表すwalkというメソッドが必要です。

このうちwalkについては、デフォルト実装("walking")を用意しています。コード中のコメントを参考に、何が起きているのか追ってみてください。

// `trait トレイト名 {..}`でトレイトを定義
trait DuckLike {
    // トレイトを実装する型が実装すべきメソッドを定義
    fn quack(&self);

    // デフォルトメソッドを定義することもできる
    fn walk(&self) {
      println!("walking");
    }
}

// トレイトを実装するためだけのデータ型にはUnit構造体が便利
struct Duck;

// `impl トレイト名 for 型名 {..}`で定義可能
impl DuckLike for Duck {
    // トレイトで実装されていないメソッドを実装側で定義する
    fn quack(&self) {
        println!("quack");
    }
}

struct Tsuchinoko;

// 別の型にも実装できます。
impl DuckLike for Tsuchinoko {
    fn quack(&self) {
        // どうやらこのツチノコの正体はネコだったようです
        println!("mew");
    }

    // デフォルトメソッドを上書きすることもできる
    fn walk(&self) {
        println!("wriggling");
    }
}

// 既存の型にトレイトを実装することもできる
// モンキーパッチをしているような気分
impl DuckLike for i64 {
    fn quack(&self) {
        for _ in 0..*self {
            println!("quack");
        }
    }
}

fn main() {
    let duck = Duck;
    let tsuchinoko = Tsuchinoko;
    let i = 3;
    duck.quack(); // => quack
    tsuchinoko.quack(); // => mew
    i.quack(); // => quack; quack; quack
}
1

トレイト

このように素晴らしい抽象化を提供してくれるトレイトですが、なんと、トレイトを使うコストはゼロです(静的ディスパッチ)。トレイトを使っても使わなくても、プログラムの速度が変わらないのです。そのため、他の言語でクラスの継承を用いて動的ディスパッチをするよりも高速に動作します。

トレイトは、Rustが掲げるゼロコスト抽象化のひとつの実践例だといえるでしょう。

トレイト境界

「あるトレイトを実装する型」をジェネリクスで受け取ることもできます。これを、トレイト境界といいます。型は値の集合を定義するのに対し、トレイト境界は型の集合を定義するものだと考えられます(集合の集合を扱っているようで、変な気分になりますね)

下記に、トレイト境界を使ったプログラムの例を示します。ジェネリックな関数を定義するときに、型パラメータ名: トレイト名という具合に型パラメータにトレイト境界を付けることで、その関数の本体でトレイトのメソッドが使えるようになります。

// 上記のmain以外の定義たち

// ジェネリクスの型パラメータに`型パラメータ名: トレイト名`で境界をつけることができる
fn duck_go<D: DuckLike>(duck: D) {
    // 境界をつけることで関数本体でトレイトのメソッドが使える
    duck.quack();
    duck.walk();
}

fn main() {
    let duck = Duck;
    let f = 0.0
    duck_go(duck); // => quack; walking

    // DuckLikeを実装していない型は渡せない
    // duck_go(f); // the trait `DuckLike` is not implemented for `{float}`
}
2

トレイト境界

Rustで実践的な実装 その1. rsgrep

さて、このあたりで1つ、アプリケーションを作ってみましょう。

引数として与えた正規表現で、別の引数として与えたファイルの中を検索し、マッチした行を返すというアプリケーションです。同じような挙動をするUnixコマンドにちなんで、rsgrepと名付けましょう。

Hello Cargo

ここまでのRustプログラムは、すべてrustcコマンドでコンパイルして実行するだけの単純なものでした。ここからは、ある程度まとまったアプリケーションを作っていくので、RustのビルドツールのCargoを使います。

cargoコマンドにより、プログラムのビルドのほか、パッケージのインストールやプロジェクトテンプレートの作成が可能です。

まずは、cargo newで新しいプロジェクトの雛型を作りましょう。

$ cargo new rsgrep --bin
     Created binary (application) `rsgrep` project
$ cd rsgrep

--binというのは、このプロジェクトがライブラリではなく、実行可能なプログラムのものである(後述するbinクレートであること)を示しています。

プロジェクトの雛形ができたら、cargo runとすることでテンプレートを走らせてみましょう。cargo newによって生成される雛形には、あらかじめHello Worldプログラムが用意されているので、下記のような結果になるはずです。

$ cargo run
   Compiling rsgrep v0.1.0 (file:///home/kim/Rust/rsgrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.23 secs
     Running `target/debug/rsgrep`
Hello, world!

最初のコード

Rustのソースコードはsrc以下に置きます。実行のエントリポイントとなるのはmain.rsファイルです。cargo newした直後は、下記のような内容になっているはずです。

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

このファイルを編集してアプリケーションを作っていきます。いきなり完成を目指すのではなく、まずは引数で受け取ったファイルの中身を1行ずつプリントできるようにしてみましょう。

最初に、標準ライブラリからいくつかの機能をインポートします。インポートには、他の多くの言語のように、プログラムの冒頭で次のように指定します。

// stdクレートのfsモジュールにあるFile型をインポート。以後は`File`として参照できる。
use std::fs::File;

// 同じモジュールから複数インポートする際は`{}`でまとめて指定できる。
use std::io::{BufReader, BufRead};

// モジュール全体をインポートすることもできる。
use std::env;

Rustでは、プログラムをクレートという単位で管理しています。今まで作ってきたプログラムのように単体で実行可能なプログラムは、すべてbinクレートとして管理されます。これに対し、ライブラリとして機能するプログラムは、libクレートとして管理されます。

クレートには、複数のモジュールが所属します。Rustでは、モジュールがプログラムの可視性を扱う単位であり、モジュールごとにエクスポートやインポートを管理できます。

モジュールは入れ子にでき、多くは1ファイル/ディレクトリで1モジュールですが、もっと細かい単位で管理することもできます。上記のuse std::fs::File;は、stdクレートのfsモジュールのFile型をインポートするという意味です。

stdクレートは、Rustで標準的に使えるモジュールが所属しているクレートで、fsのほかにもioenvなどのモジュールが所属しています。

3

クレートとモジュール

それでは、続けて一気にコード全体を掲載します。もう少しスマートな書き方もできますが、最初なので分かりやすく書いてます。細かくコメントも入れてあるので、参考にしながら内容を追ってみてください。

// stdクレートのfsモジュールにあるFile型をインポート。以後は`File`として参照できる。
use std::fs::File;

// 同じモジュールから複数インポートする際は`{}`でまとめて指定できる。
use std::io::{BufReader, BufRead};

// モジュール自体をインポートすることもできる。
use std::env;

fn usage() {
    println!("rsgrep PATTERN FILENAME")
}

fn main() {
    // envモジュールのargs関数でプログラムの引数を取得できる。
    // そのうち2番目を`nth`で取得(0番目はプログラムの名前、1番目はパターンで今は無視)。
    // 引数があるか分からないのでOptionで返される。
    let filename = match env::args().nth(2) {
        // あれば取り出す。
        Some(filename) => filename,
        // なければヘルプを表示して終了
        None => {
            usage();
            return;
        }
    };
    // `File`構造体の`open`関連関数でファイルを開ける。
    // 失敗する可能性があるので結果は`Result`で返される。
    // 下の方でもう一度`filename`を使うためにここでは`&filename`と参照で渡していることに注意。
    let file = match File::open(&filename) {
        // 成功すれば取り出す。
        Ok(file) => file,
        // ファイルが見つからないなどのエラーの場合はそのままプログラム終了
        Err(e) => {
            println!("An error occurred while opening file {}:{}", filename, e);
            return;
        }
    };
    // Fileをそのまま使うと遅いのと`lines`メソッドを使うために`BufReader`に包む。
    // この`new`もただの関連関数。
    let input = BufReader::new(file);
    // `BufReader`が実装するトレイトの`BufRead`にある`lines`メソッドを呼び出す。
    // 返り値はイテレータなので`for`式で繰り返しができる
    for line in input.lines() {
        // 入力がUTF-8ではないなどの理由で行のパースに失敗することがあるので
        // `line`もResultに包まれている。
        let line = match line {
            Ok(line) => line,
            // 失敗したらそのまま終了することにする。
            Err(e) => {
                println!("An error occurred while reading a line {}", e);
                return;

            }
        };
        println!("{}", line);
    }
}

注目してほしいのは、Result型やOption型を多用している点です。プログラムの外の世界は怖いことだらけなので、このようにResult型やOption型で値をくるんで安全に使えるようにしています。

この状態で、プログラムをcargo runで実行してみましょう。実行の際の引数として、パターンとファイル名を与えるのを忘れずに。ここでは、プロジェクトディレクトリに作られているはずのCargo.tomlというファイルを指定して実行してみることにします。

$ cargo run -- pattern Cargo.toml
   Compiling rsgrep v0.1.0 (file:///home/kim/Rust/rsgrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.37 secs
     Running `target/debug/rsgrep Cargo.toml`
[package]
name = "rsgrep"
version = "0.1.0"
authors = ["Sunrin SHIMURA (keen) <3han5chou7@gmail.com>"]

[dependencies]

[package]から始まる6行が、皆さんの手元にあるはずのCargo.tomlの内容と一致していれば成功です。

regexとcrates.io

指定したファイルの内容をすべて出力できるようになったところで、検索したい正規表現のパターンを指定できるようにしましょう。

ここまではstdクレートしか使っていませんが、Rustでは標準ライブラリ以外にもさまざまなクレートが利用可能です。それらは、crates.ioに登録されており、Cargoから手軽に扱えるようになっています。

Cargo: packages for Rust5

crates.ioに登録されているクレートを使うには、Cargo.tomlを編集します。いまは正規表現が必要なので、crates.ioに登録されているregexクレートを使うように、次のように編集してください。

# ...

# dependenciesに依存クレートを書く
[dependencies]
# regexの0.2.1かそれ以上の互換性のあるバージョンを使う
regex = "0.2.1"

バージョンの指定では多彩な書き方が可能ですが、とりあえず欲しいバージョンを直に書いておけば大丈夫です。

この状態でビルドしてみましょうrunでもビルドが走りますが、いまは実行する必要はないのでbuildを使います)

$ cargo build
    Updating registry `https://github.com/rust-lang/crates.io-index`
   Compiling regex-syntax v0.4.0
   Compiling void v1.0.2
   Compiling libc v0.2.21
   Compiling utf8-ranges v1.0.0
   Compiling unreachable v0.1.1
   Compiling memchr v1.0.1
   Compiling thread-id v3.0.0
   Compiling thread_local v0.3.3
   Compiling aho-corasick v0.6.3
   Compiling regex v0.2.1
   Compiling rsgrep v0.1.0 (file:///home/kim/Rust/rsgrep)
    Finished dev [unoptimized + debuginfo] target(s) in 8.85 secs

上記のように、regexやその依存クレートをCargoがビルドしてくれます。

完成

regexクレートの機能を使って、rsgrepプログラムを完成させましょう。

stdではないクレートをプログラムから使うには、まずextern crate クレート名;で宣言が必要です。宣言したあとは、他のモジュールと同じようにプログラムから参照できるようになります(Unixファイルシステムのマウントにちょっと似ていますね)

6

標準ライブラリ以外のクレート

先ほどのコードに、以下の内容を追記してください。

// regexを宣言
extern crate regex;
// regexからRegex型をインポート
use regex::Regex;

// ..

fn main {
    // 引数からパターンを取り出す
    let pattern = match env::args().nth(1) {
        Some(pattern) => pattern,
        None => {
            usage();
            return;
        }
    };
    // 取り出したパターンから`Regex`をあらためて作る
    // 無効な正規表現だった場合などにはエラーが返る
    let reg = match Regex::new(&pattern) {
        Ok(reg) => reg,
        Err(e) => {
            println!("invalid regexp {}: {}", pattern, e);
            return
        }
    };

    // ..

    for line in input.lines() {
        // ..
        // パターンにマッチしたらプリントする
        // is_matchはリードオンリーなので参照型を受け取る
        if reg.is_match(&line) {
            // 上で参照型で引数に渡したので、ここでも使える
            println!("{}", line);
        }
    }

}

上記を追記したら、ファイルとパターンを指定して実行してみましょう。

# '[' で始まる行を抜き出してみる
$ cargo run -- '^[\[]' Cargo.toml
  Compiling rsgrep v0.1.0 (file:///home/kim/Rust/rsgrep)
    Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
     Running `target/debug/rsgrep '^[\[]' Cargo.toml`
[package]
[dependencies]

ちゃんと動きました!

Rustで実践的な実装 その2. アナグラム

エンジニアHubに会員登録すると
続きをお読みいただけます(無料)。
登録のメリット
  • すべての過去記事を読める
  • 過去のウェビナー動画を
    視聴できる
  • 企業やエージェントから
    スカウトが届く