pyhaya’s diary

機械学習系の記事をメインで書きます

Rustで練習がてら簡単な切符の予約システムを作ってみる

最近、Rustを始めました。しばらくはドキュメントを見ながら勉強していたのですが、飽きてきて、何か作りたいなと思い始めたので、(すごく)簡単な切符の予約システムを作ってみました。まだ初心者なのでGUIで操作できたり、コマンドラインで引数を与えて実行できるというような高尚なものでは無いです。どちらかというと、自分みたいに勉強したはいいけど何していいか全くわからないという人に、初心者でもこんなことができるということを知ってもらいたいというのが目的です。

リファクタリングの過程とかも書くので、結果だけみたい人は目次で飛んでください。

環境

準備

cargo new reserve

でプロジェクトを作ります。

大雑把な外枠を作る

src/main.rs

struct Request {
    start: String,
    destination: String,
    time: String,
    time_kind: String,
}

fn reserve(request: Request) -> bool {
    true
}

fn main() {
    let request = Request {
        start: "NewYork".to_string(),
        destination: "Chicago".to_string(),
        time: "18:00".to_string(),
        time_kind: "start".to_string(),
    };

    if reserve(request) {
        println!("Succeed in reserving that train");
    } else {
        println!("Failed to reserved that train");
    }
}

かなりざっくりしています。まず決めたのは、予約の詳細をRequestという名前の構造体で保持するということです。そして、予約可能かどうかを判定する関数reserveを定義しました。これは現時点ではただ単にtrueを返すだけの関数です。

改善点は山のようにあります。

  • Stringを直接渡しているので、文字列リテラル(&str)を渡すようにする。こうすれば.to_string()もなくせる
  • リクエストが妥当なものか確かめる仕組みを作る
  • reserveRequestのメソッドにする

メソッドを定義してまとめる

src/main.rs

struct Request<'a> {
    start: &'a str,
    destination: &'a str,
    time: &'a str,
    time_kind: &'a str,
}

impl<'a> Request<'a> {
    fn reserve(&self) -> bool {
        true
    }

    fn is_valid(&self) -> bool {
        if self.start == "" {
            println!("You need to determine start point");
            return false;
        } else if self.destination == "" {
            println!("You need to determine destination");
            return false;
        } else if self.time == "" && self.time_kind != "" {
            println!("Invalid time specification");
            return false;
        }

        true
    }
}

fn main() {
    let request = Request {
        start: "Tokyo",
        destination: "Kyoto",
        time: "18:00",
        time_kind: "start",
    };

    if request.is_valid() && request.reserve() {
        println!("Succeed in reserving that train");
    } else {
        println!("Failed to reserved that train");
    }
}

参照を渡すようにすることで、Rust特有の文法であるライフタイムが必要になりました('aとか書いてあるもの)。構造体の要素が参照なので、明示的にそのライフタイムを構造体自身と合わせてやる必要があります。すこしRustっぽくなってきました(?)が、肝心のreserveメソッドがなんの働きもしていません。次にココらへんを改善していきます。

  • 時刻表を作る
  • reserveメソッドを実装する
  • codeが長くなってきたのでRequestをモジュールにまとめる

リクエストをモジュールに分ける

src/reserve_request/mod.rs

use std::collections::HashMap;

pub struct Request<'a> {
    pub start: &'a str,
    pub destination: &'a str,
    pub time: &'a str,
    pub time_kind: &'a str,
}

impl<'a> Request<'a> {
    pub fn reserve(&self, timetable: HashMap<(&str, &str), &[&str]>) -> bool {
        let st = (self.start, self.destination);

        let times = timetable.get(&st);
        let default: &[&str] = &[];
        let result = match times {
            Some(r) => r,
            None => default,
        };

        for t in result {
            if *t == self.time {
                return true;
            }
        }

        false
    }

    pub fn is_valid(&self) -> bool {
        if self.start == "" {
            println!("You need to determine start point");
            return false;
        } else if self.destination == "" {
            println!("You need to determine destination");
            return false;
        } else if self.start == self.destination {
            println!("Invalid. The start point and destication is same");
        } else if self.time == "" && self.time_kind != "" {
            println!("Invalid time specification");
            return false;
        }

        true
    }
}

src以下にreserve_requestディレクトリを作り、その中にmod.rsを作ります。main.rsから使うメソッドや構造体にはpubをつけてパブリックにします。これをモジュールとして認識してもらうために、src以下にlib.rsを作ります。

src/lib.rs

pub mod reserve_request;

main.rsでは、useでreserve_requestをインポートします。

src/main.rs

use reserve::reserve_request;
use std::collections::HashMap;

fn main() {
    //TODO: save time table in the json file
    let time: &[&str] = &["12:00", "14:00", "18:00", "19:00"];
    let mut stations = HashMap::new();
    stations.insert(("Tokyo", "Kyoto"), time);

    let request = reserve_request::Request {
        start: "Tokyo",
        destination: "Kyoto",
        time: "18:00",
        time_kind: "start",
    };

    if request.is_valid() && request.reserve(stations) {
        println!("Succeed in reserving that train");
    } else {
        println!("Failed to reserved that train");
    }
}

main.rsでは、HashMapとして時刻表を保持しています。HashMapのキーは出発駅と到着駅、値が出発時刻です。これはあまりにしょぼいので、将来的にはJSONに書いて、それを読み込む形になるかと思います。

テストを書く

モジュールにも分けたことですし、reserve_requestにテストを追加します。Rustでは実際のコードと同じファイルにテストをかけるので、mod.rsにテストを書き込みます。

src/reserve_request/mod.rs

// 同じため省略

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn struct_type() {
        let request = Request {
            start: "Tokyo",
            destination: "Kyoto",
            time: "18:00",
            time_kind: "start",
        };

        assert!(request.start == "Tokyo");
        assert!(request.destination == "Kyoto");
        assert!(request.time == "18:00");
        assert!(request.time_kind == "start");
    }

    #[test]
    fn invalid_examples() {
        let start_is_lack = Request {
            start: "",
            destination: "Kyoto",
            time: "18:00",
            time_kind: "start",
        };

        assert!(!start_is_lack.is_valid());

        let destination_is_lack = Request {
            start: "Tokyo",
            destination: "",
            time: "18:00",
            time_kind: "start",
        };

        assert!(!destination_is_lack.is_valid());

        let start_is_destination = Request {
            start: "Tokyo",
            destination: "Tokyo",
            time: "18:00",
            time_kind: "start",
        };

        assert!(!start_is_destination.is_valid());

        let invalid_time_info = Request {
            start: "Tokyo",
            destination: "Kyoto",
            time: "",
            time_kind: "start",
        };

        assert!(!invalid_time_info.is_valid());
    }

    #[test]
    fn valid_examples() {
        let valid_example_1 = Request {
            start: "Tokyo",
            destination: "Kyoto",
            time: "18:00",
            time_kind: "start",
        };

        assert!(valid_example_1.is_valid());

        let valid_example_2 = Request {
            start: "Tokyo",
            destination: "Kyoto",
            time: "",
            time_kind: "",
        };

        assert!(valid_example_2.is_valid());
    }
}

Requstの関連関数をつくる

Rustではコンストラクタはなく、似たような役割のメソッドを関連関数と呼ぶみたいです(出典:ドキュメントの日本語訳)。今までは、main.rsで構造体を直接作って、形式が妥当かどうか確かめていましたが、関連関数を作って、そこから構造体を生成して妥当性チェックをするのがよいでしょう。

src/reserve_request/mod.rs

use std::collections::HashMap;

pub struct Request<'a> {
    pub start: &'a str,
    pub destination: &'a str,
    pub time: &'a str,
    pub time_kind: &'a str,
}

impl<'a> Request<'a> {
    pub fn new<'b>(
        start: &'b str,
        destination: &'b str,
        time: &'b str,
        time_kind: &'b str,
    ) -> Result<Request<'b>, &'b str> {
        let r = Request {
            start: start,
            destination: destination,
            time: time,
            time_kind: time_kind,
        };

        if r.is_valid() {
            Ok(r)
        } else {
            Err("Failed to construct request from this information")
        }
    }

    pub fn reserve(&self, timetable: HashMap<(&str, &str), &[&str]>) -> bool {
        let st = (self.start, self.destination);

        let times = timetable.get(&st);
        let default: &[&str] = &[];
        let result = match times {
            Some(r) => r,
            None => default,
        };

        for t in result {
            if *t == self.time {
                return true;
            }
        }

        false
    }

    fn is_valid(&self) -> bool {
        if self.start == "" {
            println!("You need to determine start point");
            return false;
        } else if self.destination == "" {
            println!("You need to determine destination");
            return false;
        } else if self.start == self.destination {
            println!("Invalid. The start point and destication is same");
            return false;
        } else if self.time == "" && self.time_kind != "" {
            println!("Invalid time specification");
            return false;
        }

        true
    }
}

// 以下テスト

このようにすることで、main.rsではRequest::new(...)で構造体を作れ、newの中で妥当性チェックを行えるので、is_validはプライベートにできます。

src/main.rs

use reserve::reserve_request;
use std::collections::HashMap;

fn main() {
    //TODO: save time table in the json file
    let time: &[&str] = &["12:00", "14:00", "18:00", "19:00"];
    let mut stations = HashMap::new();
    stations.insert(("Tokyo", "Kyoto"), time);

    let request = reserve_request::Request::new("Tokyo", "Kyoto", "18:00", "start").unwrap();

    if request.reserve(stations) {
        println!("Succeed in reserving that train");
    } else {
        println!("Failed to reserved that train");
    }
}

まとめ

ここまで来てもやはりかなり大雑把で、直す所だらけですが、これだけでも結構Rustという言語の良い勉強になったなと感じてます。最終的なコードも玄人から見たら「ここはこう書くべきではない」とかあると思うので、気づいたらバシバシ指摘していただけると、勉強になるので嬉しいです。

実践Rust入門[言語仕様から開発手法まで]

実践Rust入門[言語仕様から開発手法まで]