[目次] [第7回] [第9回]

Ruby勉強会(第8回)

練習7-2の回答例

練習7-1は実行してみるだけなので、説明は略す。

練習7-2のプログラム例をex7-1.rbex7-1a.rb に示す。ファイルを2つに分けたのは、電卓の基本計算機能の定義部分と テストのためのコードを一緒にすると、後で計算機能を利用し辛くなるためである。

注意点としては、引き算と割算について、スタックに先にpushされた値から 後にpushされた値を引く(割る)ようにしなくてはならないので、引き算のメソッド では、

x = @stack.pop
y = @stack.pop
z = y - x

のようにする必要がある。つまり、最初にpopした値は「後にpushされた」値 であることに注意すること。

クラスCalcを使ってユーザインタフェースを付け加える

Calcの定義のみでは実際に利用者が計算に用いるには irb よりも面倒な操作が必要になる。そこで、今回と次回でこのCalcに ユーザインタフェースを付け加えて、実際に電卓として使い易くすることにする。

ユーザインタフェースはコマンドプロンプトの中だけで使う専ら文字表示と キーボード入力によるCUI (Character User Interface)と、 ウィンドウやアイコン、メニューバーなどを絵的に表示し、キーボードに加えて マウスなどのポインティングデバイスも用いるGUI (Graphical User Interface) などが代表的である。ここではまず CUIを、次に GUI を付加することを目標にする。

CUIを定義する

CUIにも画面指向のものと行指向のものがあるが、ここでは行指向とする。 UNIX/Linuxで一般的に使用できるコマンドdcを手本に次のように 設計する。

  1. 文字列を1行入力する。
  2. 文字qが入力されれば、実行を終了する。
  3. 文字pが入力されれば、スタックトップの値を表示する。
  4. 文字+が入力されれば、足し算する。
  5. 文字-が入力されれば、引き算する。
  6. 文字*が入力されれば、掛け算する。
  7. 文字/が入力されれば、割算する。
  8. 上記以外の文字列が入力されれば、整数と見なしてスタックにpushする。
  9. 処理が終れば入力に戻る。

最後の項は厳密には数値以外のものが入力される可能性もあるが、文字列の メソッドto_iは幸い何を与えても整数に変換し、整数として解釈 できない時は0を返すので、ここではさぼることにする。

練習8-1

上の設計に従って、キーボードからの入力に従って計算する電卓を作成 せよ。完成すれば、例えば"1", "2", "+", "p" の順に入力すると3が表示される はずである。

なお、入力文字列を場合わけする際、if…elsif…elsif…else…end では記述が繁雑になるが、代わりに以下のようにcase-when文を使用することも できる。

case 式0
when 式1
    文1
when 式2
    文2
…
when 式n
    文n
else
    文x
end

まず式0の値を求め、それと一致する式nを探し、存在すれば文nを実行する。 もし一致するものがなければ文xを実行する。

例外発生時の電卓機能の改善

ここまでで作った電卓は例えば、0除算の例外が発生すると停止してしまう 他、スタックに計算に必要な値がたりなくても停止してしまうし、後者の場合は エラーメッセージの意味が利用者にはわかり難い。これでは健全なアプリケーション システムとは言えないので、例外発生時の処理を適切に行い、利用者が混乱 しないように工夫する。

例外処理

0除算などはRubyの例外として発生する。そのため、rescueによって その例外時の処理を指定する。ここでは使い勝手の問題と考え、CUIの プログラムの方に手を入れることにする。すなわち、入力文字列を判断して、 push や計算の処理を呼び出した中での例外なので、上の設計の2.〜8.を begin〜rescueで囲って、例外発生時には受け取った例外を 表すオブジェクトを画面に表示し、表示後はそのまま9.へと続ければ良い。

練習8-2

ここまでの説明に従って、0除算などの例外が発生しても実行が中断 しないように電卓プログラムを修正せよ。

例外時のスタックの扱い

さて、ここまでの説明に従ってプログラムを作成・修正しても、まだ困ったことが 生じる。それは例外は実際にRuby のプログラムで計算する際に生じるので、 計算に用いる値は既にスタックからpopされており、そのまま例外処理に突入すると その値が捨てられてしまうことである。 そのため、長い計算の途中で例外を発生させると、そこまで計算した途中結果を 捨てることになってしまう。

前出のdcは上の設計の範囲では同じ使い勝手なので、実際に 使用して比べるとわかるが、例えば0除算などが起きても、スタックにはその例外を 起こす直前の値が全て残っている。これをどうすれば良いか考えてみる。

この場合、計算で例外が起きた時にスタックは計算する直前の状態のままで あれば良いということなので、計算処理とスタックの処理の部分を改良することに なる。すなわち、修正すべきはクラスCalcの各メソッドとになる。

例えば、足し算のメソッドaddを例に考えると、2回popして値を取得し、 計算して、結果をpushしている。ここで例外が起きるのはスタックが空に なるとpopした値はnilという特別なオブジェクトになることがクラス Arrayで定義されている。このnilは足し算ができないので、 +の処理のところで例外が発生するのである。実際にはスタックが空になった ということを捉えたいので、まずCalcに独自の popメソッドを 以下のように追加する。

def pop
    if @stack.size < 1
        raise("Stack is Empty")
    end
    return @stack.pop
end

ここで、raiseというメソッドを呼んでいるが、これはわざと例外を 発生されるものである。例外オブジェクトの生成も自動的に行うので、その オブジェクトに格納するメッセージを引数で渡している。つまり、 @stack が空(サイズが0)であれば、「Stack is Empty」というメッセージを持った例外を 発生させ、そうでなければ、普通にpopした値を返す。

練習8-3

クラス定義Calcを修正して、上のpopメソッドを追加し、 各計算のメソッドの「@stack.pop」と言うところを「 pop」と 書換えて、このメソッドを呼び出すようにせよ。この修正を施した上で、実行し 計算中にスタックの値が足りなくなると、先ほどまでとは異なり、 「Stack is Empty」というメッセージが表示されるようになる。

例外時のスタックの扱い(続き)

さて、これでエラーメッセージが一部わかり易くなったが、まだスタック にあった値が捨てられる問題は直っていない。

これを修正するためには、やはり各計算のメソッド(add, sub, mul, div) を修正しなくてはならない。つまり、スタックから値をpopしてから例外が 発生したら、その既にpopした値をスタックに戻して(push)から、計算ができ なかったものとして、そのメソッドで再び例外を発生させるのである。この 修正を入れるとすると次のようになる(足し算を例にする)

def add
    x = pop
    begin
        (省略)
    rescue
        push(x)
        raise
    end
end

ここでは、スタックへの値のpushも自前のpushメソッドを使うように している。引数のないraiseを実行すると通常は直前に発生した例外と 同じ例外を発生させたことと同じになる。

練習8-4

上の修正例にならって、四則演算のメソッド全てを修正し、スタックに 値が1個しかない時に計算使用とすると、メッセージを表示するが、 スタックには計算前と同じ値が残っていることを確認せよ。

練習8-5

ここまでの修正ではまだ0除算の場合の処理が不十分である。つまり、 0除算はスタックから値を2個取り出した状態で例外が発生するため、 スタックにはその2個の値を戻してやらなければならない。これは、 基本的には練習8-4と同様のテクニックで対応できるが、2個目の値を 例外時に戻すためのbegin〜rescue〜endを追加して、その範囲を 割算部分のみに限れば良い。

0除算の場合に値が2つとも残るように 修正せよ。

テーブル駆動型プログラム

この節は若干発展的な内容を扱う。ここまでの電卓プログラムでは 入力文字列に対して、どの処理を実行すべきかをifcase で一々比較して判断していた。この方式は単純ではあるが、次のような 問題がある。

そこで、値毎に対応した処理を実行したい時には条件分岐で記述せずに その値と対応する処理を表に登録して、その表を検索し、対応する処理を呼び出す という手法がとられることがある。これはテーブル駆動型プログラムと呼ばれる。 この場合、必要な技術要素としては次の2点があげられる。

  1. 表から素早く目的の値を探索する。
  2. 表にデータの一種として、「呼び出すべきプログラムのありか」や 「実行すべきプログラムコード」を登録できる。

前者として、RubyではHashというクラスが使える(値が比較的小さな 整数だけであれば配列でも構わない)。そして、後者は「手続きオブジェクト」 (クラスProcのインスタンス)を用いることができる。

Hashの定数的な記述方法は以下の通りである。

{ key1 => value1, key2 => value2, ... keyn => valuen }

そして、これはオブジェクトであるので、そのオブジェクトを格納した 変数をaとすれば、「a[式]」と書くと、その式の値と一致する keyn と対応した valuen が取り出される。なお、一致する keynがなければ nilになる。

手続きオブジェクトは最近のRubyでは次のように書くと生成される。

lambda { [|引数リスト|] 文 ... }

引数はなくても良い。そして、このオブジェクトが変数bに入っていれば、 「b.call(引数)」によってその中に書かれた文が実行される (引数がない場合はb.callで良い)。

練習8-6

テーブル駆動型プログラムの手法を用いて電卓プログラムの 入力判断のところを書き直してみよ。 (発展的内容なので、今すぐにできなくても良い。)

GUI(予告編)

次回はGUIで電卓を作成するが、RubyによるGUI機能は Ruby/Tk と呼ばれる添付ライブラリを用いることが多い。 これは昔からある別のスクリプティング言語であるTclに付属の GUIツールキットTk をRubyから用いるものである。

Ruby/Tkで押しボタンが1個あるウィンドウを表示するプログラム例は 次のようになる。

require("tk")

TkButton.new(nil, "text"=>"今日は",
        "command"=>lambda{ puts("さようなら"); exit(0) }).pack
Tk.mainloop

このプログラムを実行すると、「今日は」と書かれたボタンがついた ウィンドウが表示され、そのボタンをクリックすると ウィンドウを閉じると同時に「さようなら」と表示して実行を終了 する。ボタンを押された時に実行する文は前出の手続きオブジェクトを用いる。

練習8-7

上のボタンを表示するプログラムを入力、実行してみよ。

(注: Ruby/Tkのプログラムを実行するためには Rubyの処理系だけでなく、 Tcl/Tkの処理系も正しくインストールされていなければならない。)


[目次] [第7回] [第9回]

by Tetsuo Sakaguchi
2007年 2月16日 金曜日 14時59分59秒 JST