Rustは実行効率や安全性を重視した人気のプログラミング言語ですが、難しいと言われることもあります。本連載ではいろいろな有名アルゴリズムを解くことでRustに慣れていきます。今回は、RPN電卓を実装してみましょう。またエラー処理についても紹介します。

  • 逆ポーランド記法を計算するRPN電卓を実行したところ

    逆ポーランド記法を計算するRPN電卓を実行したところ

RPN電卓とは

RPN電卓とは、逆ポーランド記法を利用して記述した計算式を計算する電卓のことです。逆ポーランド記法(あるいは、後置記法)とは、数字など被演算子の後で演算子を記述する記述方法です。

例えば、「8 + 9」という計算式を逆ポーランド記法で記述すると「8 9 +」となります。このような書き方は、読みづらく感じるかもしれませんが、日本語で計算を言い表すとき、無意識にこの記法を使っています。数字の後に助詞を足して見ると、「8に9を+(足す)」となります。

逆ポーランド記法と日本語の相性の良さは抜群で、「(2 × 3 + 4)÷2」のような長い計算式を表現する場合も、「2に3を×(掛けて)4を+(足して)2で÷(割る)」のように、自然な日本語で表現できます。

逆ポーランド記法はスタックを使うと簡単に計算できる

逆ポーランド記法はコンピューターと相性が良く、スタック構造を使うことで簡単に計算できます。次のような手順で計算できます。

 (1) 文字列を空白でトークンに区切る
 (2) トークンを1つ読む
 (3) 数値なら、スタックに積む
 (4) 演算子なら、スタックから2つ値を取り出して計算してスタックに積む
 (5) トークンが空でなければ(2)に戻る
 (6) スタックに残っている値が答えとなる

例えば、逆ポーランド記法「2 3 * 4 + 2 /」の計算の様子を図で確認してみましょう。特にスタックの様子に注目してみてください。次のようになります。

  • 逆ポーランド記法の計算方法

    逆ポーランド記法の計算方法

数値ならばそのままスタックへ入れて、演算子ならスタックから取り出して計算して結果をスタックに入れるという単純な仕組みであることが分かるでしょう。

RPN電卓のプログラム

それでは、上記の手順を元にしてプログラムを作成してみましょう。以下のプログラムを「rpn.rs」というファイル名で保存します。

// 逆ポーランド記法を計算する関数 --- (*1)
fn calc_rpn(text: &str) -> f64 {
    // 計算用のスタックを初期化 --- (*2)
    let mut stack: Vec<f64> = vec![];
    // 逆ポーランド記法の式をスペースで区切る --- (*3)
    let tokens = text.split(' ');
    // スペースで区切られたトークンを一つずつ処理 --- (*4)
    for tok in tokens {
        // 空ならば無視する
        let tok = tok.trim();
        if tok.len() == 0 { continue; }
        // 数値変換を試みる --- (*5)
        match tok.parse::<f64>() {
            // 変換成功ならスタックに追加 --- (*6)
            Ok(num) => {
                stack.push(num);
                continue;
            },
            // 失敗なら演算子
            Err(_) => {}
        };
        // 演算子ならスタックから2つ値を得る --- (*7)
        let b = stack.pop().unwrap_or(0.0);
        let a = stack.pop().unwrap_or(0.0);
        // 計算結果をスタックに追加 --- (*8)
        match tok {
            "+" => stack.push(a + b),
            "-" => stack.push(a - b),
            "*" => stack.push(a * b),
            "/" => stack.push(a / b),
            "%" => stack.push(a % b),
            _ => { println!("[ERROR] {}", tok); }
        }
    }
    // 最後にスタックに残っている値が計算結果 --- (*9)
    return stack.pop().unwrap_or(0.0);
}

fn main() {
    // 簡単な問題を解く --- (*10)
    let expr = "8 9 +";
    println!("{} = {}", expr, calc_rpn(expr));
    // 複雑な問題を解く1
    let expr = "2 3 * 4 + 2 /";
    println!("{} = {}", expr, calc_rpn(expr));
    // 複雑な問題を解く2
    let expr = "6 4 + 2 / 3 +";
    println!("{} = {}", expr, calc_rpn(expr));
}

プログラムを実行してみましょう。WindowsならPowerShell、macOSならターミナル.appを起動してコマンドを実行します。(Windowsの場合「/」を「¥」に読み替えてください。)

# コンパイル
$ rustc ./rpn.rs
# 実行
$ ./rpn

すると次のように実行結果が表示されます。「8 9 +」と「2 3 * 4 + 2 /」と「6 4 + 2 / 3 +」の実行結果が正確に表示されました。

  • RPN電卓のプログラムを実行したところ

    RPN電卓のプログラムを実行したところ

プログラムを確認してみましょう。

(*1)では逆ポーランド記法を計算する関数calc_rpnを定義します。文字列参照(&str)を受け取り、64ビット浮動小数点(f64)型の値を返す関数です。

(*2)では計算に使うスタックを初期化します。スタックは計算しやすいよう64ビット浮動小数点のベクタ型とします。ベクタとは動的に複数の値を追加・削除できるデータ型です。

(*3)では引数として与えられた逆ポーランド記法のテキストをスペースで区切ります。そして、(*4)以降では区切った1つずつの値(トークンと呼びます)をfor文で1つずつ処理します。

(*5)では全てのトークンを数値に変換しようと試みます。matchを使うと、parseメソッドが成功した時(Ok)と失敗した時(Err)を分岐させることができます。成功した時には、(*6)でスタックに数値を追加します。

(*7)以降は数値以外の時の処理を記述します。ここでは数値以外では演算子しかないので、演算子の時は、スタックから2つ値を取得します。

(*8)ではmatchを利用して演算子の種類に合わせて計算を行います。そして、結果をスタックに追加します。このように、matchは幅広いパターンマッチングに対応します。

トークンを全て読み終わった後、(*9)ではスタックから1つ値を取り出して、それを関数の戻り値とします。

最後の(*10)では、main関数の中で、3回、関数calc_rpnを呼び出して結果を表示します。

Rustのエラー処理について

Rustでは安全に処理を行うために、失敗する可能性があるメソッドには、Result型が用いられることが多いです。これは、Ok(T)かErr(E)を返すデータ型です。今回も文字列型を浮動小数点型に変換するparseメソッドの戻り値や、ベクタ型からデータを取り出すpopメソッドの戻り値に使われていました。 以下は、文字列参照(&str)型で指定した"3.1415"という文字列をf64型に変換するプログラムです。parseメソッドが成功したら変換した値numを返し、失敗したら0.0を返すようにしています。

fn main() {
    let s = "3.1415";
    let f = match s.parse::<f64>() {
        Ok(num) => num,
        Err(_) => 0.0
    };
    println!("{}", f);
}

ただし、ちょっとしたことで毎回matchを使うのは面倒ですよね。そこで、matchを使わなくても成功した時、失敗した時の値を手軽に指定できる方法が用意されています。それが、unwrapやunwrap_orメソッドです。上記のmatchを使ったプログラムをunwrap_orで書き直すと以下のようになります。

fn main() {
    let s = "3.1415";
    let f = s.parse::<f64>().unwrap_or(0.0);
    println!("{}", f);
}

matchを使ったものと比べると、かなり短くなりました。Rustではunwrapやunwrap_orがよく出てきますが、これを見たらエラー処理を短く書いているというのが分かります。

まとめ

以上、今回はRPN電卓の作成に挑戦してみました。Pythonなど他のプログラミング言語と比べると、matchを利用したエラー処理が少し独特に見えます。しかし、その他の部分はそれほど違いがあるわけではありません。そう思うと、今回のプログラムに関しては、Rustのエラー処理に精通してしまえば、苦なく使いこなせるようになるのかもしれません。

自由型プログラマー。くじらはんどにて、プログラミングの楽しさを伝える活動をしている。代表作に、日本語プログラミング言語「なでしこ」 、テキスト音楽「サクラ」など。2001年オンラインソフト大賞入賞、2004年度未踏ユース スーパークリエータ認定、2010年 OSS貢献者章受賞。技術書も多く執筆している。直近では、「シゴトがはかどる Python自動処理の教科書(マイナビ出版)」「すぐに使える!業務で実践できる! PythonによるAI・機械学習・深層学習アプリのつくり方 TensorFlow2対応(ソシム)」「マンガでざっくり学ぶPython(マイナビ出版)」など。