[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 は通常に戻す(+,- とか未定義の状態)とかできればいいんだけどな。
何か方法があるのかな。