Rustで練習がてら簡単な切符の予約システムを作ってみる
最近、Rustを始めました。しばらくはドキュメントを見ながら勉強していたのですが、飽きてきて、何か作りたいなと思い始めたので、(すごく)簡単な切符の予約システムを作ってみました。まだ初心者なのでGUIで操作できたり、コマンドラインで引数を与えて実行できるというような高尚なものでは無いです。どちらかというと、自分みたいに勉強したはいいけど何していいか全くわからないという人に、初心者でもこんなことができるということを知ってもらいたいというのが目的です。
リファクタリングの過程とかも書くので、結果だけみたい人は目次で飛んでください。
環境
- Ubuntu 18.04
- cargo 1.36.0
準備
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
を返すだけの関数です。
改善点は山のようにあります。
メソッドを定義してまとめる
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という言語の良い勉強になったなと感じてます。最終的なコードも玄人から見たら「ここはこう書くべきではない」とかあると思うので、気づいたらバシバシ指摘していただけると、勉強になるので嬉しいです。
- 作者: κeen,河野達也,小松礼人
- 出版社/メーカー: 技術評論社
- 発売日: 2019/05/08
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る