はじめに
実践Rustプログラミング入門のChapter4を写経しました。その感想です。ソースコードは下記githubにあります*1。
github.com
やったこと
逆ポーランド記法の数式を計算するアプリケーションの作成をしました*2。本書では核となる計算処理だけでなく外回りもRustの機能を利用することでいろいろ入門させてくれました。具体的には下記です。
- コマンドライン引数の処理
- 計算ロジックの実装
- ユニットテストの記述
- エラーハンドリング
コマンドライン引数の処理
CUIアプリケーションの場合、コマンドライン引数の扱いは最初の悩みどころです。関数std::env::args()及びクレートclapを利用した方法が紹介されています。clapを利用することで良い感じに処理してくれます*3。
#[derive(Clap, Debug)] #[clap( name = "My RPN program", version = "1.0.0", author = "yu2 ta7ka", about = "Super awesome sample RPN calculator" )] struct Opts { /// Sets the level of verbosity #[clap(short, long)] verbose: bool, /// Formulas written in RPN #[clap(name = "FILE")] formula_file: Option<String>, }
計算ロジックの実装
fn eval_inner(&self, tokens: &mut Vec<&str>) -> Result<i32> { let mut stack = Vec::new(); let mut pos = 0; while let Some(token) = tokens.pop() { pos += 1; if let Ok(x) = token.parse::<i32>() { stack.push(x); } else { let y = stack.pop().context(format!("invalid syntax at {}", pos))?; let x = stack.pop().context(format!("invalid syntax at {}", pos))?; let res = match token { "+" => x + y, "-" => x - y, "*" => x * y, "/" => x / y, "%" => x % y, _ => bail!("invalid token at {}", pos), }; stack.push(res); } // -v オプションが指定されている場合、この時点でのトークンとスタックの状態を出力する if self.0 { println!("{:?} {:?}", tokens, stack); } } ensure!(stack.len() == 1, "invalid syntax"); Ok(stack[0]) }
ユニットテストの記述
RustのビルドツールCargoにはテストを行える仕組みがあります*4。これを使うことでソースコード中にテストが書けます。これでコード更新のたびにデグレがないか即座にチェックできますね。
#[cfg(test)] mod tests { use super::*; #[test] fn test_ok() { let calc = RpnCalculator::new(false); assert_eq!(calc.eval("5").unwrap(), 5); assert_eq!(calc.eval("50").unwrap(), 50); assert_eq!(calc.eval("-50").unwrap(), -50); assert_eq!(calc.eval("2 3 +").unwrap(), 5); assert_eq!(calc.eval("2 3 *".unwrap()), 6); assert_eq!(calc.eval("2 3 -").unwrap(), -1); assert_eq!(calc.eval("2 3 /").unwrap(), 0); assert_eq!(calc.eval("2 3 %").unwrap(), 2); } #[test] fn test_ng(){ let calc = RpnCalculator::new(false); assert!(calc.eval("").is_err()); assert!(calc.eval("1 1 1 +").is_err()); assert!(calc.eval("+ 1 1").is_err()); } }
エラーハンドリング
最後にエラーハンドリングです。RustにはResult< T, E > というエラーハンドリング用の型が定義されています。Rustに例外処理はありません。この型を利用して独自にエラー定義することが可能ですが、手間です。
そこで、エラー処理クレートが存在しているので、これを利用します。
use anyhow::{bail, ensure, Context, Result}; fn get_int_from_file() -> Result<i32> { let path = "number.txt"; let num_str = std::fs::read_to_string(path) .with_context(|| format!("failed to read string from {}", path))?; if num_str.len() >= 10 { bail!("it may be too large number"); } ensure!(num_str.starts_with("1"), "first digit is not 1"); num_str .trim() .parse::<i32>() .map(|t| t * 2) .context("failed to parse string") } fn main() { match get_int_from_file() { Ok(x) => println!("{}", x), Err(e) => println!("{:#?}", e), } }
気づいたこと
書いていて楽しい!Rustはやりたいことをかなりそのままソースコードにできるように感じていてそれが楽しさに繋がっていると思っています。ただし、やりたいことをしっかりと抽象化しないとグダグダなコードになってしまうので、実装者の能力がもろにコードに出る気がします*5。
クレートを活用しよう!すでに先人がいろいろ用意してくれているのでそれらの仕様を理解してどんどん活用する癖をつけていくべきと思います。本書ではメジャーなクレートを紹介してくれており感謝です。あとクレート追加でハマることもあるので、対応の練習になりました。
トレイトで実装を汎用化する!Rustにはトレイトという機能/概念があります。トレイトの機能は一言で説明が難しく、私はまだ説明できる自信がありません。今回の写経を通してトレイトの意味を少し掴めたかなと思います。
fn run<R: BufRead>(reader: R, verbose: bool) -> Result<()>
この関数定義は、BufReadトレイトを実装した型ならば第一引数として正しい型であるという意味を持ちます。BufReadトレイトを実装した型は複数存在しており、複数種類の型の入力を許容しており汎用化/抽象化を実現しています。これはトレイトの機能の一部ですが、トレイトは実装を抽象化する*6ためにいろいろ頑張ってくれていると少し理解が進んだ気がします。
おわりに
実践Rustプログラミング入門はタイトル通り実践的に入門させてくれる良書だと思います。今回のChapter4は実践入門の最初のChapterになります。ここからWebアプリ、組み込みなど多様に展開していくので凄まじいです。Rustを少し触ってもっと知りたいという方には超オススメな本だと思います!