今回は小規模なgemなので、このまま「/lib/rpn_calculator.rb」中のRpnCalculatorモジュールにアルゴリズムを書いてしまってもよいです。ただし、ここでは、拡張性や美しさを考え、StackCalculatorクラスを作成することにします。
gem内でクラスを作成する場合、名前空間の汚染を避けるためにgemのモジュール(ここではRpnCalculatorモジュール)内にクラスを作ります。今回の場合だと、「RpnCalculator::StackCalculator」となります。
また、クラスを記述したソースコードを置く場所も、名前空間に対応するようにしましょう。今回作成するクラスは「RpnCalculator::StackCalculator」なので、「/lib/rpn_calculator/stack_calculator.rb」にクラスの内容を書いていきます。
では、StackCalculatorクラスのコードを以下に示します。まずはザッと眺めてみて、どのようなコードになっているかを考えてみてください。
module RpnCalculator class StackCalculator def initialize(formula) @formula = formula @stack = [] end def calc @formula.each do |e| if numeric?(e) @stack.push(e) else op1 = @stack.pop.to_f op2 = @stack.pop.to_f result = op2.send(e, op1) @stack.push(result) end end @stack.first end private def numeric?(s) begin Float(s) true rescue ArgumentError false end end end end
1行目でRpnCalculatorモジュールの宣言が、2行目でStackCalculatorクラスの宣言が始まっています。このように入れ子になっているのは、StackCalculatorクラスをRpnCalculatorモジュールに閉じ込めるためです。
では、各メソッドの役割について見ていきましょう。
さらに詳細に解説します。
数式から取り出してきた値が文字列などの非数値オブジェクトかもしれないので、数値に変換できるかをチェックする必要があります。
「Float(s)」は「s.to_f」のように、変数「s」を実数に変換した値を返します。ただし、「s.to_f」だと実数として無効な文字列の場合0.0が返ってくるのに対して、「Float(s)」は無効な文字列だとArgumentErrorが発生します。
このように、numeric?メソッドでは、「Float(s)」の挙動を利用して「s」が実数に変換できるかをチェックします。
アルゴリズムの本体です。
9行目で「@formula」から要素を1つずつ取り出し、10行目で実数に変換できるかをチェックします。もし変換できるならば、被演算子だとみなして、11行目でスタックに要素を積みます。もし変換できなければ、演算子だとみなして13行目以降の演算を行います。
13行目と14行目でスタックの上から値を2つ取り出し、実数に変換してから15行目で演算を行います。
15行目では、ちょっとしたメタプログラミングのトリックを使っています。「send」メソッドは、レシーバーのオブジェクトのメソッドを呼ぶためのメソッドです。例えば、以下の3行のコードは等価です。
alice.say("hi") alice.send(:say, "hi") alice.send("say", "hi")
演算子もメソッドであることを思い出してください! もし「e」に「-」という文字列が格納されている場合、15行目では以下のようなコードが実行されることになります。
result = op2.send("-", op1)
「-」メソッドは引数を1つとり、自分自身と引数の値を減算したものを返すので、結果的に「op2 - op1」という演算が行われ、変数「result」に格納されます。
もし、sendを使ったメタプログラミングのトリックを使わない場合、対応させたい演算子の種類の数だけ、以下のようなコードを延々と書くことになってしまいます。
case e when "+" op2 + op1 when "-" op2 - op1 when "*" op2 * op1 : : end
これは明らかに無駄ですよね? このように、メタプログラミングを駆使すれば、少ない行でたくさんのことができるようになるのです!
17行目で「result」変数に格納された演算結果をスタックに積み、eachループの先頭に戻ります。
全ての数式の要素を処理したら、21行目でスタックの先頭の内容を返してメソッドは終了します。
せっかく作ったStackCalculatorも、RpnCalculatorから呼び出してあげないと役に立ちません。「/lib/rpn_calculator.rb」に変更を加えましょう。
require "rpn_calculator/version" require "rpn_calculator/stack_calculator" module RpnCalculator class << self def run(args) calculator = StackCalculator.new(args) puts calculator.calc end end end
2行目の「require "rpn_calculator/stack_calculator"」を書き忘れると、stack_calculator.rbが読み込まれなくなるので、エラーで止まってしまいます。
ここでは、7行目でStackCalculatorオブジェクトを生成し、変数「calculator」に格納しています。8行目でcalculatorのcalcメソッドを呼び出すことで計算させ、その出力をターミナルに出力しています。
では、動作確認をしてみましょう! プロジェクトのルートディレクトリで、以下のコマンドを実行してみてください。
$ bundle exec bin/rpn_calculator 10.5 2 7 + -
ここまでの作業で間違いがなければ、以下のような出力が得られるでしょう。
$ bundle exec bin/rpn_calculator 10.5 2 7 + - 1.5
きちんと計算できていますね!
Copyright © ITmedia, Inc. All Rights Reserved.