hirax.net::inside out::2010年05月12日

最新記事(inside out)へ  |   年と月を指定して記事を読む(クリック!)

2010年4月 を読む << 2010年5月 を読む >> 2010年6月 を読む

2010-05-12[n年前へ]

オブジェクト指向のRubyに、数式処理ソフトウェアMathematicaの機能を自然な形で取り入れてみよう!? 

 「Rubyで数独ソルバを書いてみる」でMathematicaで書かれたコードをRubyに翻訳してみました。その作業をしながら考えたことは、「Mathematicaの機能をRubyに自然に取り込むとしたら、一体どういう言語仕様になるのだろうか」ということです。すべてのものが「リスト」であり、基本的に関数型言語であるMathematicaの機能を、多くのものを「オブジェクト」として扱うオブジェクト指向のRubyに(Rubyになじんだやり方で)取り入れるとしたら、一体どんな記述になるのだろう?という疑問です。

 RubyとMathematicaを繋いだものとして、Ruby/Mathematicaがあります。Ruby/Mathematicaで「Cos(x)をxで積分した結果」を求めるコードは下のようになります。

math=Mathematica::Mathematica.new.start
puts math.eval_foreground('Integrate[Cos[x], x]')
 このコードは'Sin[x]'を出力しますが、「オブジェクト指向」のRubyに溶け込んでいるとは言えない記述になっています。

 そこで、頭の整理がてら、もしもMathematicaのコードをRubyに溶け込ませるとしたらどうなるかを考えながら、「Mathematicaの機能を(.NETベースの)IronRubyに取り込むコード」をつらつら書いてみました。

 書いたコード(ライブラリ)は後述することとして、まずは、そのコードを使った例を紹介してみることにします。たとえば、「Cos(x)をxで積分した結果」を求めるなら、こんな感じの記述になります。

p 'x'.Cos.Integrate 'x'
これは、'xという文字列オブジェクトにCosメソッドを適用し、さらに"x"で積分するというメッセージを投げる、という具合の記述です。コードをそのまま読んでいくなら、「'x'のコサイン関数をxで積分する」という具合になります。

 あるいは、'2 x + 3 == 0'という方程式を、xについて解くなら、

'2 x + 3 == 0'.Solve 'x'
という具合の記述になります。何だか、少し「Rubyみたいな」コードに見えてくるのではないでしょうか?

 同様に、「(Rubyネイティブの!)配列"[[1,2,3],2,3]"について、それぞれのサイン関数を求め、数値化する(その結果を出力する)コード」なら、こんな具合です。

p ([[1,2,3],2,3]).map.Sin.N
 ここで、"map"はRubyのメソッドで、"Sin"と"N"はMathematicaの関数です。RubyのメソッドとMathematicaの関数が入り混じっていることがわかります。それらは、頭文字が大文字なのがMathematicaの関数で、頭文字が小文字なのがRubyの関数ということで区別することができます。RubyとMathematicaでは命名規則が異なるので、名前がぶつかることはありません。  ちなみに、
p 3.Times(10).Plus 5
というコードを書けば、これは「3に10を乗算した結果に5を足した結果を出力する」ということになります。この"Times"も"Plus"も、あたかもRubyっぽく見えますが、いずれもMathematicaの機能(関数)です。

 さて、オブジェクト指向のRubyにMathematicaの機能を自然な形で取り入れるために、試しに書いてみたコードは、下のようになります。

 …下のコードを要約すると、メソッド名が見つからない場合に呼ばれるmethod_missingで、「見つからないメソッドがあった場合には、その命令をMathematicaに投げちゃえ」というだけのコードです。つまりは、単に(Ruby上で)未知のメソッドを「右(Ruby)から左(Mathematica)に受け流す」という「ムーディ勝山」方式…ということになります。しかも、オブジェクトの総本山、Objectクラスにその「ムーディ勝山」メソッドをいきなりMix-inしてしまう…という、とんでもないコードです。一発ネタ的コードとして、楽しんで頂けたら幸いです。

# 2010/05/12 jun hirabayashi jun@hirax.net

class Mathematica
  require 'Wolfram.NETLink'
  include Wolfram::NETLink
  # ここにMathKernel.exeへのパスを記述する
  PAR="-linkmode launch -linkname 'C:...\\MathKernel.exe'"

  def Mathematica.to_m(array)
    strArray=[]
    array.each do |item|
      if item.kind_of? Array
	strArray<<Mathematica.to_m(item) 
      else
	if item.kind_of? String
	  strArray<<'"'+item+'"'
	else
	  strArray<<item
	end
      end
    end
    '{'+strArray.join(',')+'}'
  end
  
  def Mathematica.to_a str
    require "JSONParser" 
    jsonParser=JSONParser.new
    s=str.gsub(/, ([^{]{1})/,',"\1')
    s=s.gsub(/([^}]{1}),/,'\1",')
    s.gsub!(/\{([^{]{1})/,'["\1')
    s.gsub!(/([^}]{1})\}/,'\1"]')
    jsonParser.parse s    
  end

  def Mathematica.callback
    proc {
      @kernelLink.EvaluateToInputForm 'MVClose[]',0
    }
  end

  def initialize()    
    ObjectSpace.define_finalizer self,Mathematica.callback
    @kernelLink=MathLinkFactory.CreateKernelLink PAR
    @kernelLink.WaitAndDiscardAnswer
  end
  
  def do(q)
     @kernelLink.EvaluateToInputForm q,0
  end
  
end

module MathematicaModule
  def method_missing(name, *args)
    $mathematica=Mathematica.new unless $mathematica
    if self.kind_of? Array
      obj=Mathematica.to_m(self) 
    else
      obj=self 
    end 
    if name.to_s
      results=$mathematica.do( [name,'[',
          ([obj]+args).join(','),']'].join('') )  
    end
    return results
  end

  def To_a
    Mathematica.to_a self
  end
end

class Object
  include MathematicaModule
end