これは、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は変換する前に、Fixnum
、String
、Symbol
の+
などの演算子メソッド(?)を上書きします。具体的には、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
さて、上のalias
はgenerate
が終わったら戻さないといけません。さっきの逆をします。
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の解説をしているのはおそらく自分だけだと思います(そして大した技術じゃない)。それに加えて遅れるという、完全に浮いてしまうか!と思っていますが、アドベントカレンダーは今年が初めてなので、生温かくスルーしていただければと思います。
自分が作ったものをこういう場で解説するのは少し恥ずかしい///のですが、突っ走れということで気にしません。多分後で後悔すると思います。