12/19の課題について

解答例

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