SyntaxHighlighter

StackEdit CSS

2013年12月13日金曜日

ratex(gem)の解説

これは、Ruby Advent Calendar 2013の11日目の記事です。完全に忘れてました…なんらかの通知メールが来てくれればいいのだが…

この日の前の方々の記事からして、自分の記事は自作のgemの解説とかでいいのかよ…と思いつつ書きます。(浮くの覚悟です)

ratexは、Rubyの式をTeXの式に変換するgemです。使い方は Rubyの式をTeXの数式に変換するやつ作ったを見てください。ソースコードはhttps://github.com/long-long-float/ratexにあります。

この解説は勉強の一環で書いたもので、間違っているかもしれません。そういうときは優しくツッコんでもらうと喜びます。

1 + 1 ≠ 2

1 + 1

の挙動から説明します。ratexは変換する前に、FixnumStringSymbol+などの演算子メソッド(?)を上書きします。具体的には、aliasしてから定義します。

OPERATORS = [:+, :-, :*, :/, :**, :==, :+@, :-@, :<, :>]
KLASSES = [Fixnum, String, Symbol]

KLASSES.each do |klass|
    klass.class_eval do
        OPERATORS.each do |ope|
            if method_defined? ope
                alias_method "#{ope}_", ope
            end
        end

        #色々な演算子を定義する…
    end
end

ここで例えば、+を下のように定義すると、1 + 1が文字列として返ってきます。

def +(other)
    "#{self} + #{other}"
end

さて、上のaliasgenerateが終わったら戻さないといけません。さっきの逆をします。

KLASSES.each do |klass|
    klass.class_eval do
        OPERATORS.each do |ope|
            remove_method ope
            if method_defined? "#{ope}_"
                alias_method ope, "#{ope}_"
            end
        end
    end
end

これにより、

1 * 2 + 3 * 4 + 5

というようなやや複雑(?)な式も普通に変換出来ます。どういうことかというと*+より結合度が高いので、

"1 * 2" + "3 * 4" + 5
"1 * 2 + 3 * 4" + 5
"1 * 2 + 3 * 4 + 5"

となります。Rubyのパーサーをいい感じに利用することで、結構手抜きできました。試していないのでわかりませんが多分、構文木も起こせるかもしれません。

Contextの導入

上だけでは簡単な計算、しかもFixnum,String,Symbolしか使えません。xなどの変数も使いたくなるでしょう。そこで、Contextというクラスを導入しました。

class Context
    def method_missing(name, *args)
        name.to_s
    end
end

と定義して、Contextのインスタンスに対して変換したい式をinstance_evalに渡せばx + 1のような式も動くんじゃねという魂胆です。Contextという名前は他に名前が思いつかなくて、なんとなくこれな気がしたので付けただけです。このテクニック(?)は、内部DSLで使われているようです(というかこれで知った気がする)。

さて、これで

i + 1

が動くようになりました。

sinなどの関数も扱いたくなりました。そういう時はContext

def sin(expr)
    "\\sin(#{expr})"
end

と書けばちゃんと動いてくれます。調子に乗って、sqrtも作ってみました。

def sqrt(expr, n = 2)
    "\\sqrt" + ((n != 2)? "[#{n}]" : "") + "{#{expr.to_s}}"
end
Ratex.generate{ sqrt(x) } # => "$$\\sqrt +  + {x}$$"

おかしいです。sqrtの式がそのまま変換されてしまったようです。そうです。aliasしていたのを忘れてました。aliasするのをbegin_generate, 戻すのをfinish_generateとすると、

def out_of_generate
    finish_generate
    ret = yield
    begin_generate
    ret
end

を定義して

def sqrt(expr, n = 2)
    @gen.out_of_generate do
        "\\sqrt" + ((n != 2)? "[#{n}]" : "") + "{#{expr.to_s}}"
    end
end

とすればちゃんと動くようになります。ブロック便利ですね。

out_of_generateを至るところに書いていくのですが、遅くならないのか?と思われるかもしれません。パフォーマンス度外視です。

not = but ==

===となってしまいました。=は、

def ほげほげ=(other)
    #...
end

という形で、ほげほげを書かないといけないので無理と判断しました。Context内に

def method_missing(name, *args)
    #...
    if ret =~ /(%w+)=/
        return "#{ret} = #{args[0]}"
    end
    #...
end

と書けば呼ばれるのでは?と思ったのですが、

Ratex.generate{ v = r * i }

としても呼ばれません。

v = nil
Ratex.generate{ v = r * i }
p v #=> "r + i"

どうやらローカル変数と解釈されたようです。

終わりに

投稿が遅れてしまいました。前にもアドベントカレンダーに投稿させてもらったのですが、これも遅れてしまいました。遅刻は病気かもしれないと言う話がありますが、どうなんでしょう?

最初にも書きましたが自作のgemの解説をしているのはおそらく自分だけだと思います(そして大した技術じゃない)。それに加えて遅れるという、完全に浮いてしまうか!と思っていますが、アドベントカレンダーは今年が初めてなので、生温かくスルーしていただければと思います。

自分が作ったものをこういう場で解説するのは少し恥ずかしい///のですが、突っ走れということで気にしません。多分後で後悔すると思います。

0 件のコメント:

コメントを投稿