龍虎氏の記事に対するコメント。
逆ポーランド記法の電卓に付いては以前、このブログでも扱っている。
また、isamさんのブログの方でも、それこそRustでの逆ポーランド記法電卓のコードを書く試みが行われている。
前から逆ポーランド記法ってどういう意味があるのかと思ってたけど、なるほどスタックと相性が良いと言う話で納得、確かに!これは為になった
これも何度か言ってるけど、逆ポーランド記法電卓は「実際にこの世にハードウェアとして存在した/してる」マシンだ。
有名なトコではHP(ヒューレッドパッカード)社製造の電卓(の一部)がそれだ。
これがハードウェア的にまさしく「スタックマシン」となってて、そのスタックマシンの構造のままに打ち込む・・・入力に「逆ポーランド記法」を要する。
スタックマシンの構造をそのまま使う「逆ポーランド記法」は複雑な構文解析を要さない。結果、Lispのように「構文木をそのまま入力する」ような仕組みになってるわけだ。
また繰り返すが、Apple IIを開発したスティーヴ・ウォズニアックが、Apple社を設立する際に資金源として売っぱらった電卓がこのHP社製の逆ポーランド記法の関数電卓であり、亡くなった任天堂の元社長、岩田聡氏が初めてプログラミングを覚えた、のもHP社製の逆ポーランド記法の関数電卓だった(※1)。
が、期待はちょっと裏切られたメイン関数にあらかじめ数式を打ち込んでおくタイプか~・・まあ、良いけど。
RustはC言語系言語な見た目の割には教育手順はそれに沿ってないケースが多く、入力は後回しにされる傾向がある。
いや、教育観点から見るとそれは「良いこと」なんだ。何でも入力を扱って・・・と言うPascalやBASICのような教育手順に比べると遥かにいいと思う。
ただ、ぶっちゃけると、入力に纏わる手順が(危険を度外視した)C言語に比べると遥かにややこしい。記述が多い。どっちかっつーとJavaに近い、ってカンジのメンド臭さだ。
加えると、RustはPascal/OCaml/F#のような「強い型付け」言語だ。よって型変換が煩わしい(笑)。大体のトコ、コンパイルエラーは「ロジックの間違い」ではなくって、型の整合性が無い辺りに現れる。動的型付け言語に慣れてるとイライラが半端じゃなくなる(笑)。
特に、Rustはstr型とString型がまた別で、入力が文字列な以上、「どっちがどっちだったっけ?」となりやすい。Rust初心者にはキツいだろう。従って「記述が増えまくるだろう」入力に付いては後回し、ってのは方針としてはそうせざるを得なくなる。
一方、「インタラクティブな構造のプログラム」自体の設計は簡単だろう・・・REPLを組めばいいだけ、だ。このお題の場合、calc_rpnと言う関数をEvaluator(評価器)としてmain関数内でループさせればいいだけ、とはなる。・・・このように、だ。
fn main() {
loop {
print!(">>> ");
io::stdout()
.flush()
.expect("プロンプトのフラッシュに失敗hしました");
let mut expr = String::new();
io::stdin()
.read_line(&mut expr)
.expect("行の読み込みに失敗しました");
let expr = expr.trim();
if expr.eq_ignore_ascii_case("q") {
break;
}
println!("{} = {}", expr, calc_rpn(&expr));
}
}
厄介なのがプロンプト代わりのprint!による表示だ。ここが恐らく、フツーに書けばプロンプトとしての役目を果たさない・・・計算結果が出てから表示されるだろう(当然、プロンプトは入力処理が成される前に表示されないといけない)。
Rustの構造のせいなのか、いつぞや見た要flushの問題を抱えている。つまり、出力バッファを一旦掃除しないとprint!が上手く動かない。
そんなわけでバッファ掃除のコードが挿入されてるわけだが、こんなん、Rust初心者やプログラミング初心者の想像の範疇外だろう。
やっぱRustに於いては最初にあまり入出力に纏わるアレコレを取り上げたくない、ってのは分かる気がする(笑)。
とにかくメンドイ、んだ。
なお、例によって「関数型プログラミング」っぽいコードを紹介する。
// 逆ポーランド記法を計算する関数
fn calc_rpn(text: &str) -> f64 {
// 逆ポーランド記法の式をスペースで区切る
text.split_whitespace()
.fold(vec![], |mut stack, tok| {
let tok = tok.trim();
// 空ならば無視する
if tok.len() == 0 {
stack
} else {
match tok.parse::<f64>() {
// 変換成功ならスタックに追加
Ok(num) => {
stack.push(num);
stack
},
// 失敗なら演算子
Err(_) => {
// 演算子ならスタックから2つ値を得る
let b = stack.pop().unwrap_or(0.0);
let a = stack.pop().unwrap_or(0.0);
// 計算結果をスタックに追加
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); }
}
stack
}
}
}
})
.pop().unwrap_or(0.0)
}
あくまで関数型っぽい、だ。実際はfoldを採用しただけ、でロジックの殆どは原作と大して違わない。ぶっちゃけ、労多くして得る物は少ない、ってカンジかも。
最大の問題は、結果、破壊的変更であるpopとpushの採用だ。これを避ける事が可能か?と色々試してみたんだけど、どーにも上手く行かなかった。
結果、Racketでのコードのように上手く纏まらなかったんだよなぁ。
(define (calc-rpn text)
(let ((table (hash "+" + "-" - "*" * "/" / "%" modulo)))
(car
(foldl (lambda (tok stack)
(when (non-empty-string? tok)
(cond ((string->number tok) => (lambda (x)
(cons x stack)))
(else (let ((b (car stack)) (a (cadr stack)))
(cons ((hash-ref table tok) a b) (cddr stack)))))))
'() (string-split text)))))
Racketだと上のように、ファーストクラスオブジェクトである関数をハッシュテーブルに登録して、変数tokが文字列だった場合、テーブルから関数を引っ張ってきてスタックから引っこ抜いてきた変数aとbへと適用出来る。
Rustでもクロージャをハッシュテーブルの値として登録可能だけど、それを関数書式に則って変数aとbへと適用出来ない。色々試してはみたんだけど、すんなり行かないんだ。この辺はPythonの方が圧倒的に柔軟で、Lispと同じような記述が可能なんだけど、Rustでは現時点では難しい、と言う事が分かった。
とにかく、RustはC言語より強力な言語な事は確かなんだけど、関数型にしたから、と言ってコードが短くなるとは必ずしも限らない。一般に「記述が長くなる」と言う事は「望ましくないプログラムになる」と同義だとは思う。
よってこのケースでも、素直に破壊的変更前提でプログラミングした方が良いかも。
また、とにかくロジックそのものより「型変換の煩わしさ」がついて回るのがRustの特徴といえば特徴なのかなぁ。あまりプログラミングのロジックと関係ねぇトコでエラーを食らう、と(笑)。大体のトコ、「型」の不整合で、その辺解決するのが「浅くRustと関わってる」程度では思いつくのが難しい、とは言えると思う。
まぁ、今回はこんなトコかな。
※1: HP社製の当時の関数電卓にはBASICが内蔵されていて、プログラミングが可能だった・・・と言う事は、いわゆる電卓と言うより、実質的には後のポケコンに近い。