■ 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では静的な型を持たないので継承は実 装の継承(親クラスで定義されているメソッドなどの定義を受け継ぐ)のみとし て使われるためです.