Rubyで逆ポーランド変換機を作りgem作成&コマンドの使い方若手エンジニア/初心者のためのRuby 2.1入門(13)(3/4 ページ)

» 2015年02月06日 18時00分 公開
[著:麻田優真、監修:山根剛司,株式会社アジャイルウェア]

クラスファイルにアルゴリズムを閉じ込める

 今回は小規模なgemなので、このまま「/lib/rpn_calculator.rb」中のRpnCalculatorモジュールにアルゴリズムを書いてしまってもよいです。ただし、ここでは、拡張性や美しさを考え、StackCalculatorクラスを作成することにします。

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
/lib/rpn_calculator/stack_calculator.rb

 1行目でRpnCalculatorモジュールの宣言が、2行目でStackCalculatorクラスの宣言が始まっています。このように入れ子になっているのは、StackCalculatorクラスをRpnCalculatorモジュールに閉じ込めるためです。

 では、各メソッドの役割について見ていきましょう。

  • initialize
    「StackCalculator.new」を実行したときに呼ばれるコンストラクター。ここでクラス変数「@formula」「@stack」を初期化する
  • numeric?
    引数の値が実数(Float)に変換できるかをチェックするためのメソッド。例外によるちょっとしたトリックを使っている(後述)
  • calc
    「@formula」に格納された数式を1つずつ取り出し、スタック「@stack」を使って計算するメソッド。アルゴリズムのメインとなる

 さらに詳細に解説します。

  • numeric?メソッド

 数式から取り出してきた値が文字列などの非数値オブジェクトかもしれないので、数値に変換できるかをチェックする必要があります。

 「Float(s)」は「s.to_f」のように、変数「s」を実数に変換した値を返します。ただし、「s.to_f」だと実数として無効な文字列の場合0.0が返ってくるのに対して、「Float(s)」は無効な文字列だとArgumentErrorが発生します。

 このように、numeric?メソッドでは、「Float(s)」の挙動を利用して「s」が実数に変換できるかをチェックします。

  • calcメソッド

 アルゴリズムの本体です。

 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行目でスタックの先頭の内容を返してメソッドは終了します。

RpnCalculatorモジュールから呼び出す

 せっかく作った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
/lib/rpn_calculator.rb

 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.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。