[ruby]メタプログラミングで数式を扱う

ruby で数式を扱えるよう方法を考えてみた。
具体的には

f = -:x**2 + 3*:x + 1
eval(f)

みたいな記法ができるようにする。
別にできたからってどうとかではないけど。
オレオレ DSL を作るときにつかえないかなーなんて思ってメモ。
(あんまりちゃんとテストしてないので不具合があるかもしれない。)

コードは以下

#!/bin/env ruby
# -*- coding: utf-8 -*-

class Exp
  @@defvar = nil
  def initialize(var=0)
    @val = var
  end
  def bind(op, other)
    Exp.new([op, self, Exp.new(other)])
  end
  def eval()
    # 葉に到達したらそれぞれのすうちを評価
    if @val.is_a? Numeric
      @val
    elsif @val.is_a? Symbol
      raise "undefined variable :#{@val}" unless @@defvar.has_key? @val
      @@defvar[@val]
    elsif @val.is_a? Exp
      @val.eval
    elsif @val.is_a? Array and @val.length == 3
      op, a, b = @val
      # 再帰評価
      (a.eval).send(op, b.eval)
    else
      raise "invalid expression! (#{@val})"
    end
  end
  # ruby の標準パーサを利用して構文木を構築する
  %w[+ - / * **].each do |op|
    define_method(op) {|other| bind(op, other)}
  end
  def -@()
    bind(:*, -1)
  end
  def +@()
    bind(:*, +1)
  end

  def coerce(other)
    [Exp.new(other), self]
  end
  def self.eval(exp, defvar)
    @@defvar = defvar
    ret = Exp.new(exp).eval
    @@defvar = nil
    ret
  end
end

class Fixnum
  def coerce(other)
    if other.is_a? Exp
      [other, Exp.new(self)]
    else
      super
    end
  end
end

# 変数宣言
x = Exp.new(:x)
y = Exp.new(:y)

# 数式代入
z = (x + y)*2+3*x*y**2

puts Exp::eval(z, {:x=>4, :y=>8})  # 792
puts Exp::eval(z, {:x=>8, :y=>-1}) # 38
puts Exp::eval(z, {:x=>8.9, :y=>0}) # 17.8

z = (x + y)*2.43-3.32*x/y**2

# hash で変数定義して評価する
puts Exp::eval(z, {:x=>8.9, :y=>0.32}) #  -266.1500874999999

# 変数宣言
a = Exp.new(:a)
b = Exp.new(:b)
c = Exp.new(:c)

d = a+b+c # 数式代入
z = 0.003*(d*2+3*4**(b**(a/100.0)*0.234)*0.2)/c

puts Exp::eval(z, {:a=>1.2, :b=>8, :c=>19}) # 0.00903738074762227
# Exp::eval(z, {:a=>1.2, :b=>8}) #=> undefined variable :c

Exp クラスを作って、数式を @val に突っ込んで遅延評価させてるだけ。
自分でパーサを作るのは面倒だし、できるだけ ruby の構文をそのまま使えると便利なので数式用の演算子 +,-,/,*,-@,+@ を定義して ruby に評価させる。
数式が評価されるたびに Exp をノードとする構文木ができる。それを Exp::eval にて評価する。
3*a みたいに Fixnum を先に記述したときにエラーにならないように Fixnum の coerce を拡張している。

さらに、いちいち Exp.new(:a) とか書くのが面倒なので Symbol を変数として扱うようにする。

# symbol を使ってより書きやすくする
# Exp と同じく ruby パーサが評価するときに Exp クラスを読んで構文木を構築する。
class Symbol
  %w[+ - / * **].each do |op|
    define_method(op) do |other|
      Exp.new(self).send(op, Exp.new(other))
      end
  end
  def -@()
    bind(:*, -1)
  end
  def +@()
    bind(:*, +1)
  end
  def coerce(other)
    if other.is_a? Numeric
      [Exp.new(other), Exp.new(self)]
    else
      super
    end
  end
end

# Fixnum の coerce を更に拡張
class Fixnum
  def coerce(other)
    if other.is_a? Symbol
      [Exp.new(other), Exp.new(self)]
    else
      super
    end
  end
end

# symbol を変数として扱える
c = :a + :b
d = c * 0.332
e = d**2
f = 2 + :a

puts Exp::eval(e, {:a=>3.4, :b=>8})
puts Exp::eval(f, {:a=>3.4, :b=>8})

f *= :a # こういうのもできる
puts Exp::eval(f, {:a=>3.4, :b=>8})

func = -:x**2 + :a*:x + 0.3

puts Exp::eval(func, :x=>3, :a=>2)


cell = []
cell[0] = :x+:y
cell[1] = :x*:y
cell[2] = :x**2-:y**3

[0,2,4.5].zip([5,2.1,0]).each do |x,y|
  puts "x,y = #{x} #{y}"
  cell.each do |exp|
    puts Exp::eval(exp, :x=>x, :y=>y)
  end
end

こういう open class 的なものはあるスコープ限定でみたいなことをやって、他の場所では Symbol は通常に戻す(+,- とか未定義の状態)とかできればいいんだけどな。
何か方法があるのかな。