YU2TA7KA's BLOG ~take one step at a time~

派生開発、組み込み開発周りのこと。

Rustのアプリケーション開発入門~逆ポーランド記法計算機を通して~

はじめに

実践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])
    }

核となる計算はスタックとmatch式で実現しています。返り値Resultとそれに対応する各処理でのエラー制御の美しさがRustの魅力の一つだと思います。

ユニットテストの記述

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),
    }
}

エラーの可能性がある箇所では、発生したときのテキストを設定してそれを返すようになっています。わかりやすいですね。そして、main()では正常時と異常時を分けて出力するようになっています。

気づいたこと

書いていて楽しい!Rustはやりたいことをかなりそのままソースコードにできるように感じていてそれが楽しさに繋がっていると思っています。ただし、やりたいことをしっかりと抽象化しないとグダグダなコードになってしまうので、実装者の能力がもろにコードに出る気がします*5

クレートを活用しよう!すでに先人がいろいろ用意してくれているのでそれらの仕様を理解してどんどん活用する癖をつけていくべきと思います。本書ではメジャーなクレートを紹介してくれており感謝です。あとクレート追加でハマることもあるので、対応の練習になりました。

トレイトで実装を汎用化する!Rustにはトレイトという機能/概念があります。トレイトの機能は一言で説明が難しく、私はまだ説明できる自信がありません。今回の写経を通してトレイトの意味を少し掴めたかなと思います。

fn run<R: BufRead>(reader: R, verbose: bool) -> Result<()>

この関数定義は、BufReadトレイトを実装した型ならば第一引数として正しい型であるという意味を持ちます。BufReadトレイトを実装した型は複数存在しており、複数種類の型の入力を許容しており汎用化/抽象化を実現しています。これはトレイトの機能の一部ですが、トレイトは実装を抽象化する*6ためにいろいろ頑張ってくれていると少し理解が進んだ気がします。

おわりに

実践Rustプログラミング入門はタイトル通り実践的に入門させてくれる良書だと思います。今回のChapter4は実践入門の最初のChapterになります。ここからWebアプリ、組み込みなど多様に展開していくので凄まじいです。Rustを少し触ってもっと知りたいという方には超オススメな本だと思います!

*1:環境構築に少し手間取りましたが、README.mdの参考が参考になりました

*2:逆ポーランド記法では、スタックのデータ構造を利用することで計算処理を実現することができます。そのため、実装が難しすぎず簡単すぎないのでプログラミングの入門課題として良いです。

*3:入力の型チェック、入力エラー処理、helpコマンドの対応とかありがたいです

*4:ありがてぇ!

*5:Rustに限らないですが、より差が如実ではと

*6:ひいてはゼロコスト抽象化へ