■ 0. 目次 1. 変数を含む式 穴埋め 2. 式の微分 穴埋め 3. 発展 3.1 式のクラス階層化 3.2 演算子の拡張 3.3 パーサー 3.4 ソースコード等 4. 感想 ■ 1. 変数を含む式 穴埋め (1) left = evaluate(@left, env) (2) right = evaluate(@right, env) 実行例と確かに一致することを確認した。 ■ 2. 式の微分 穴埋め class Expr def exp_d(var) l = @left r = @right l_d = d(l, var) r_d = d(r, var) if @op == "+" then Expr.new(l_d, "+", r_d) elsif @op == "-" then Expr.new(l_d, "-", r_d) elsif @op == "*" then Expr.new(Expr.new(l_d, "*", r), "+", Expr.new(l, "*", r_d)) elsif @op == "/" then Expr.new(Expr.new(Expr.new(l_d, "*", r), "-", Expr.new(l, "*", r_d)), "/", Expr.new(r, "*", r)) else 0 end end end 実行例と確かに一致することを確認した。 ■ 3. 発展 □ 3.1 式のクラス階層化 下図のように表されるようなクラス階層に変更した。 class Expr | +-- class NumberExpr # 数(整数,実数) | +-- class VarExpr # 変数 | +-- class Arg1Expr # 引数が1つの関数 | | | +-- class ExpExpr # exp(???) | | | +-- class LogExpr # log(???) | | | +-- class SinExpr # sin(???) | | | +-- class CosExpr # cos(???) | +-- class Arg2Expr # 引数が2つの関数 | +-- class AddExpr # ??? + ??? | +-- class SubExpr # ??? - ??? | +-- class MulExpr # ??? * ??? | +-- class DivExpr # ??? / ??? 関数のクラスに引数の個数によって分けた基底クラスを持たせることにより、 コンストラクタや、to_s メソッドの記述を一箇所にまとめることができた。 グローバル関数となっていた evaluate, d はなくし、 それぞれクラスのメソッド eval, exp_d を用いられることを想定している。 □ 3.2 演算子の拡張 上のクラス階層図に含まれているとおり、算術関数 exp, log, sin, cos を ひとまず扱えるようにした。 その他の関数の追加は、クラスを作成し、後述の $ARG1EXPR_TABLE を更新して 実現できる。 □ 3.3 パーサー Rubyでは型をオブジェクトのように変数に代入できるという面白いことを知ったので、 演算子・関数名を表す文字列とそのクラス名の対応を表すハッシュのグローバル定数を $ARG1EXPR_TABLE, $ARG2EXPR_TABLE として作り、 それを用いてオブジェクトの生成をするようにした。 □ 3.4 ソースコード ソースコードは5つのファイルに分けて作った。 irbで動作させる際は expr.rb だけを読み込めばよいようにしてある。 expr.rb --- メインのファイル、他の全てのソースコードをロードする expr_base.rb --- Expr, VarExpr, NumberExpr クラスの記述があるファイル expr_arg1.rb --- 引数が1つの関数のクラスの記述があるファイル expr_arg2.rb --- 引数が2つの関数のクラスの記述があるファイル expr_parse.rb --- パーサーの記述があるファイル 動作が正しいことを確認するためのコードを記述したファイルも作った。 verify.rb がそれである。 以下にそれらのソースコードを示す。 ################################################################################ # expr.rb # ################################################################################ load "./expr_base.rb" load "./expr_arg1.rb" load "./expr_arg2.rb" load "./expr_parse.rb" ################################################################################ # expr_base.rb # ################################################################################ # # すべての HogeExpr の基底クラス # class Expr def eval(env) end def exp_d(var) end def to_s end end # # 変数を表現するクラス # class VarExpr def initialize(n) @name = n end def eval(env) env[@name].to_f end def exp_d(var) if @name == var NumberExpr.new(1.0); else NumberExpr.new(0.0); end end def to_s @name end end # # 定数を表現するクラス # class NumberExpr < Expr def initialize(n) @num = n; end def eval(env) @num.to_f end def exp_d(var) NumberExpr.new(0.0); end def to_s @num.to_s end end ################################################################################ # expr_arg1.rb # ################################################################################ # # 引数が1つの関数の基底クラス # class Arg1Expr < Expr def initialize(a) @arg = a end def to_s @op + "(" + @arg.to_s + ")" end end class ExpExpr < Arg1Expr def initialize(a) super(a) @op = "exp" end def eval(env) Math.exp(@arg.eval(env)) end def exp_d(var) MulExpr.new(@arg.exp_d(var), ExpExpr.new(@arg)) end end class LogExpr < Arg1Expr def initialize(a) super(a) @op = "log" end def eval(env) Math.log(@arg.eval(env)) end def exp_d(var) DivExpr.new(@arg.exp_d(var), @arg) end end class SinExpr < Arg1Expr def initialize(a) super(a) @op = "sin" end def eval(env) Math.sin(@arg.eval(env)) end def exp_d(var) MulExpr.new(@arg.exp_d(var), CosExpr.new(@arg)) end end class CosExpr < Arg1Expr def initialize(a) super(a) @op = "cos" end def eval(env) Math.cos(@arg.eval(env)) end def exp_d(var) SubExpr.new(NumberExpr.new(0.0), MulExpr.new(@arg.exp_d(var), SinExpr.new(@arg))) end end ################################################################################ # expr_arg2.rb # ################################################################################ # # 引数が2つの関数(演算子)の基底クラス # class Arg2Expr < Expr def initialize(l, r) @left = l @right = r end def to_s "(" + @left.to_s + @op + @right.to_s + ")" end end class AddExpr < Arg2Expr def initialize(l, r) super(l, r) @op = "+" end def eval(env) @left.eval(env) + @right.eval(env) end def exp_d(var) AddExpr.new(@left.exp_d(var), @right.exp_d(var)); end end class SubExpr < Arg2Expr def initialize(l, r) super(l, r) @op = "-" end def eval(env) @left.eval(env) - @right.eval(env) end def exp_d(var) SubExpr.new(@left.exp_d(var), @right.exp_d(var)); end end class MulExpr < Arg2Expr def initialize(l, r) super(l, r) @op = "*" end def eval(env) @left.eval(env) * @right.eval(env) end def exp_d(var) l = MulExpr.new(@left.exp_d(var), @right) r = MulExpr.new(@left, @right.exp_d(var)) AddExpr.new(l, r) end end class DivExpr < Arg2Expr def initialize(l, r) super(l, r) @op = "/" end def eval(env) @left.eval(env) / @right.eval(env) end def exp_d(var) l = MulExpr.new(@left.exp_d(var), @right) r = MulExpr.new(@left, @right.exp_d(var)) DivExpr.new(SubExpr.new(l, r), MulExpr.new(@right, @right)) end end ################################################################################ # expr_parse.rb # ################################################################################ # 文字列とクラスとの対応を表現するハッシュのグローバル定数 $ARG1EXPR_TABLE = { "exp"=>ExpExpr, "log"=>LogExpr, "sin"=>SinExpr, "cos"=>CosExpr } $ARG2EXPR_TABLE = { "+"=>AddExpr, "-"=>SubExpr, "*"=>MulExpr, "/"=>DivExpr } # その文字コードは数字のものか?を返す関数 def is_digit(c) c.kind_of?(Integer) && '0'[0] <= c && c <= '9'[0] end # その文字コードはアルファベットのものか?を返す関数 def is_alpha(c) c.kind_of?(Integer) && 'A'[0] <= c && c <= 'z'[0] end # # { 次のExprオブジェクト, 残りの文字列 } の配列を返す関数 # def next_expr(str) if str[0] == '+'[0] && !is_digit(str[1]) # "+hoge" のようにプラス記号から始まり、かつhogeが数値以外のとき # プラス記号をスキップし次の文字からのオブジェクトを返す next_expr(str[1, str.length - 1]) # return elsif str[0] == '-'[0] && !is_digit(str[1]) # "-hoge" のようにマイナス記号から始まり、かつhogeが数値以外のとき # 0-hoge という形のオブジェクトを作って返す res = next_expr(str[1, str.length - 1]) res[0] = SubExpr.new(NumberExpr.new(0), res[0]) res # return elsif str[0] == '('[0] # "(hoge)" のとき # 対応する閉じ括弧の位置を探す nst = 1 len = 1 while len < str.length && nst > 0 if str[len] == '('[0] nst += 1 elsif str[len] == ')'[0] nst -= 1 end len += 1 end # 対応する閉じ括弧が見つからなければシンタックスエラー if nst != 0 raise "Syntax Error" end # parse を(間接的に)再帰的に呼び出して、返すオブジェクトを作る res = Array.new(2); res[0] = parse(str[1, len-2]) res[1] = str[len, str.length - len] res # return elsif is_alpha(str[0]) # "hoge" のように文字列のとき(変数か引数が1つの関数) # 終端を探す len = 1 while len < str.length && is_alpha(str[len]) len += 1 end # 変数名または関数名、そして残りの文字列 nam = str[0, len] lst = str[len, str.length - len] if $ARG1EXPR_TABLE[nam] != nil # 引数が1つの関数であったとき # 自身を再帰的に呼び出して、続く引数部分のオブジェクトを作り、その上で返すオブジェクトを作る res = next_expr(lst); res[0] = $ARG1EXPR_TABLE[nam].new(res[0]) res # return else # 変数であったとき # オブジェクトを作って返す res = Array.new(2); res[0] = VarExpr.new(nam) res[1] = lst res # return end elsif str[0] == '+'[0] || str[0] == '-'[0] || is_digit(str[0]) # 数字のとき # 終端を探す(小数点が含まれているかもしれないことに注意している) len = 1 dot = false while len < str.length && (is_digit(str[len]) || (dot == false && dot = (str[len] == '.'[0]))) len += 1 end # オブジェクトを作って返す res = Array.new(2); res[0] = NumberExpr.new(str[0, len].to_f) res[1] = str[len, str.length - len] res # return else # 上のいずれでもなければ、理解不能なタームが来ているので、シンタックスエラー raise "Syntax Error" end end # # スタックで表現されている式を1つのExprオブジェクトにまとめる関数 # def reduce(stack) while stack.length > 1 r = stack.pop o = stack.pop l = stack.pop stack.push($ARG2EXPR_TABLE[o].new(l, r)) end end # # 文字列をパースし、対応するExprオブジェクトを返す関数 # def parse(str) stack = Array.new; # 一番初めの項をスタックにまず乗せる tmp = next_expr(str) stack.push(tmp[0]) str = tmp[1] while str.length > 0 op = str[0, 1] str = str[1, str.length - 1] if '*/'.index(op) != nil # 掛け算か割り算 # スタックの一番上の項に掛け算・割り算を作用させる nxt = next_expr(str) tmp = stack.pop stack.push($ARG2EXPR_TABLE[op].new(tmp, nxt[0])) str = nxt[1] elsif '+-'.index(op) != nil # 足し算か引き算 # まずスタック上の式をまとめてしまう reduce(stack) # スタックに足し算・引き算を乗せる nxt = next_expr(str) stack.push(op) stack.push(nxt[0]) str = nxt[1] else raise "Syntax Error" end end # スタックをまとめて、結果を返す reduce(stack) stack.pop # return end ################################################################################ # verify.rb # ################################################################################ load "./expr.rb" def ve(a, b) print a f = true b.each { |k,v| if f print "|" f = false else print "," end print k + "=" + v.to_s } print " --- " + parse(a).eval(b).to_s + "\n" end def ve_d(a, b, c) print "d(" + a + ")/dx|" + b + "=" + c.to_s print " --- " + parse(a).exp_d(b).eval({b=>c}).to_s + "\n" end def verify puts "【変数を含む式】" ve("x+(y-x*x*x)", {"x"=>3,"y"=>4}) ve("x+(y-x*x*x)", {"x"=>1,"y"=>10}) ve("x", {"x"=>3}) ve("30", {"x"=>3}) puts puts puts "【式の微分】" ve_d("x/(x+3)", "x", 3) ve_d("x/(x+3)", "x", 5) ve_d("x-x*x*x", "x", 2) ve_d("x-x*x*x", "x", 0) puts puts puts "【演算子の拡張】" ve("exp(1)", {}) ve("exp(4)", {}) ve_d("exp(x*x)", "x", 2) puts ve("log(1)", {}) ve("log(exp(100))", {}) ve_d("log(x*x)", "x", 4) puts ve("sin(3.1415/6)", {}) ve_d("sin(x)", "x", 0) ve_d("cos(x/2)", "x", 3.1415) puts end verify の実行結果は下のようになる。 【変数を含む式】 x+(y-x*x*x)|x=3,y=4 --- -20.0 x+(y-x*x*x)|x=1,y=10 --- 10.0 x|x=3 --- 3.0 30|x=3 --- 30.0 【式の微分】 d(x/(x+3))/dx|x=3 --- 0.0833333333333333 d(x/(x+3))/dx|x=5 --- 0.046875 d(x-x*x*x)/dx|x=2 --- -11.0 d(x-x*x*x)/dx|x=0 --- 1.0 【演算子の拡張】 exp(1) --- 2.71828182845905 exp(4) --- 54.5981500331442 d(exp(x*x))/dx|x=2 --- 218.392600132577 log(1) --- 0.0 log(exp(100)) --- 100.0 d(log(x*x))/dx|x=4 --- 0.5 sin(3.1415/6) --- 0.499986626546633 d(sin(x))/dx|x=0 --- 1.0 d(cos(x/2))/dx|x=3.1415 --- -0.499999999463457見事な解答です.パーサでは括弧の処理を文字列処理で扱っていますが,素直に 再帰下降パーサで実装した方が簡潔に書けそうな気がします.
なお,プログラム中ではVarExprはExprのサブクラスとしては定義されていませ んが,問題なく動きます.これは,Rubyでは静的な型を持たないので継承は実 装の継承(親クラスで定義されているメソッドなどの定義を受け継ぐ)のみとし て使われるためです.