Rubyメタプログラミング読んで思いついたたわごと
きっかけ
『メタプログラミングRuby』を読み直す。再読して気づいたけど、書いてあることの大半は、Rubyのオブジェクトモデルの解説だ。言い換えると、Rubyにおいてクラスやメソッド、インスタンス変数がどんなふうに作られて、実際にはどこに置かれているのか、メソッドが呼び出された時裏でどのような処理が動いているのか、などについてだ。もちろんというか、RubyのVMがどう動いてるかとかそういうレベルの話じゃなくて、Rubyのソースコードがどのようなモデルに基づいて解釈されるかってことだ、。
それをまとめると、次のようになるようだ。
- Rubyのクラス定義やメソッド定義は、宣言じゃない。コンパイル時じゃなくて実行時に『実行されて』クラスやメソッドを定義する。
- Rubyの構文には、それと等価なメソッド呼び出しが存在する
- defによる関数定義と等価なdefine_methodメソッド
- classによるクラス定義と等価なClass.newメソッド
- @a = 1のような代入や参照と等価なメソッド
- メソッド呼び出しと等価なsendメソッド
- その他
これらを上手く活用することで、実行時にクラスを生成したり、メソッドを生成したり、インスタンス変数を作り出したり、動的に呼び出すメソッドを変えたりすることができる。
メタ度が足りねーなと一瞬思った
Rubyがクラスやメソッドを動的に定義していることを考えれば、定義のための構文相当のメソッドがあることは自然なことであり、なーんだそれを書き換えるだけかー、と私は思った。
そういうんじゃない色々とアレなメソッド、evalについても触れられているのはわずかで、そこで私は「構文木を操作できるくらいじゃないとメタじゃねーな」とかLispかぶれなことを考えた。
でも、リストで表現された構文木を操作できたらメタプログラミングなのかっていうと、それはメタプログラミングのひとつではあれ、メタプログラミングのすべてではない。そもそもメタプログラミングとは何か。ウィキペディアにはこうある。
メタプログラミング (metaprogramming) とはプログラミング技法の一種で、ロジックを直接コーディングするのではなく、あるパターンをもったロジックを生成する高位ロジックによってプログラミングを行う方法、またその高位ロジックを定義する方法のこと。主に対象言語に埋め込まれたマクロ言語によって行われる。
うむ。つまりこういうことだろ?
メタプログラミング=コード生成
あるパターンに従うコード群があった場合、実際にコードを書くのではなく、略記法を考える。そして、その略記法を実際のコードに機械的に変換することで、開発効率を上げたり、意図するところを明確にすることができる。
これを実装レベルで考えると、あるパターンに従うコードの略記法としてのDSLをパースし、パターンに従うコードを自動生成するということだ。
すなわちメタプログラミングの考え方はコード生成から説明できる。
だよね?
/: : : : : __: :/: : ::/: : ://: : :/l::|: : :i: :l: : :ヽ: : :丶: : 丶ヾ ___ /;,, : : : //::/: : 7l,;:≠-::/: : / .l::|: : :l: :|;,,;!: : :!l: : :i: : : :|: : ::、 / ヽ /ヽヽ: ://: :!:,X~::|: /;,,;,/: :/ リ!: ::/ノ l`ヽl !: : |: : : :l: :l: リ / そ そ お \ /: : ヽヾ/: : l/::l |/|||llllヾ,、 / |: :/ , -==、 l\:::|: : : :|i: | / う う 前 | . /: : : //ヾ ; :|!: イ、||ll|||||::|| ノノ イ|||||||ヾ、 |: ::|!: : イ: ::|/ な 思 が /: : ://: : :ヽソ::ヽl |{ i||ll"ン ´ i| l|||l"l `|: /|: : /'!/l ん う ∠: : : ~: : : : : : : :丶ゝ-―- , ー=z_ソ |/ ハメ;, :: ::|. だ ん i|::ハ: : : : : : : : : : : 、ヘヘヘヘ 、 ヘヘヘヘヘ /: : : : : \,|. ろ な |!l |: : : : : : : : :、: ::\ 、-―-, / : : :丶;,,;,:ミヽ う ら 丶: :ハ、lヽ: :ヽ: : ::\__ `~ " /: : ト; lヽ) ゝ レ `| `、l`、>=ニ´ , _´ : :} ` / ,,、r"^~´"''''"t-`r、 _ -、 ´ヽノ \ノ / お ・ ,;'~ _r-- 、__ ~f、_>'、_ | で 前 ・ f~ ,;" ~"t___ ミ、 ^'t | は ん ・ ," ,~ ヾ~'-、__ ミ_ξ丶 | な 中 ・ ;' ,イ .. ヽ_ ヾ、0ヽ丶 l / ( ;":: |: :: .. .`, ヾ 丶 ! \____/ ;;;; :: 入:: :: :: l`ー-、 )l ヾ 丶 "~、ソ:: :い:: : \_ ノ , ヾ 丶
その発想で考えるとどういうことになるかというと。
たとえば、インスタンス変数に対する型通りのアクセサメソッドを定義することを考えてみる。アクセサメソッドの定義は次のような決まりきった形になる。
class Moge def hoge() @hoge end def hoge=(v) @hoge=v end end
こんなものはいちいちタイプするより、自動生成したい。あるクラスの中で、対象にしたいインスタンス変数を列挙しておくと、そこに勝手にアクセサメソッドを挿入してくれるようなコードの前処理器を作りたいとしよう。
じゃあ、まずはパーサを作ろう(おもむろにlexとyaccを取り出して)
ないないそれはない。もっとすでにある解析器でもって一瞬で解析できる方法にするのがいい。
たとえばコードに埋め込むだけなら一行コメントを正規表現で抜き出せばいい。もっと複雑な場合でも、コメントに埋め込んだXMLなりJSONなりYAMLなり、そういうもので表現すればすでにあるライブラリで解析できる。
今回の場合は、コメントに埋め込むようにしてみようか。行頭に'#gen-accessor 'というパターンがあったら、その後に続く識別子へのアクセサを埋め込むことにしよう。識別子は1つ以上のスペースあるいはタブで区切られる。また、Ruby識別子に不適切な名前だった場合は無視される。
この働きをするフィルタスクリプトはRubyで簡単に書ける。*1
#!/usr/bin/ruby # -*- encoding: utf-8 -*- # 1行ごとに順に処理していく。 while line = ARGF.gets do line.chomp! unless /^#gen_accessor (.*$)/ # パターンに合致しなければそのまま出力する。 puts line else # 合致した場合はどうするか # 解析器で記法を解析し、生成器でコードを生成する # ...というほど大した感じでもないが $1.splite(/\s+/).each do |property| # ここがコードの解析器。単にsplitするだけ if property =~ /[a-z]\w*/ then # ここがコード生成器。文字列に変数を埋め込むだけ。簡単だ。 puts <<-EOS # 生成されるゲッターメソッド def #{property}() @#{property} end # 生成されるセッターメソッド def #{property}=(v) @#{property}=v end EOS end end end end
これでコードを前処理すればアクセサを自動定義できる。やったねたえちゃん! 自動化できるよ!
もっとRubyのコードに
ところで、実行時に定義できるような言語、たとえばRubyなら、define_methodとinstance_variable_[set|get]を使ってメソッドを定義できる。つまり、コード生成器はいらない。
さらに、その場合はインスタンス変数のリストもコメントではなく、単なるRubyの文字列のリストとして記述できる。これで読取り器もいらない。
class Class # メソッドの定義をするのはクラス内。 # 自動定義するためには、オブジェクトとしてのクラスにメソッドを定義してやればいい # 複数の文字列を受け取るメソッドにしよう def define_accessor *plist plist.each do |property| self.class_eval do # ゲッターメソッドの動的定義 define_method property do return instance_variable_get("@#{property}") end # セッターメソッドの動的定義 define_method "#{property}=" do |v| return instance_variable_set("@#{property}", v) end end end end end class SampleClass # 使ってみる define_accessor 'hoge', 'fuga', 'piyo' end s = SampleClass.new s.hoge = 'hoge' puts s.hoge #=> hoge #=> nil
これってattr_accessorじゃないですかー! やだー!
/ ̄ ̄\ / ノ \ \ | (●)(●) | . | (__人__) | ごめんね | ` ⌒´ ノ . | } . ヽ } ヽ ノ \ / く \ \ | \ \ \ | |ヽ、二⌒)、 \
何が言いたいかというと
Rubyの動的な機能を利用することでコードジェネレータを簡単に作るのと等しい効果を得ることができる。
所感
メタプログラミングってやっぱコード生成じゃないかな……限定的なジェネレータだけでも十分に効果がある。とはいえ、コードを書き出すスクリプトを書くのは微妙にめんどいし、生成ルールが増えてくるとフィルタを正しい順番で適用するだけで手に負えなくなってくるだろう。
ところが、プログラミング言語自体の力を使うことで、コード生成はぐっと簡単になる。プログラミング言語処理系にはコードを解析する機能も、クラスやメソッドを定義する機能もある。それを再利用してやることで書くべきコードは減る。
それがコンパイラ内にあるとき、それを最利用することは難しいが、実行時に利用できるなら、コードを生成するのと実質的に同じことができる。さらに、文法もプログラミング言語のものを流用することで、パーサを作成する必要もない。そういう使い方ができる言語はメタプログラミングに向いているといえるだろう。
ところでメタプログラミングといえばLISP
LISPのマクロはコード生成により近い。だが、Rubyのコード生成をするよりずっと楽だ。
- リストであればread関数で読み込める
- 生成するのは文字列ではなくリストであればよい
readで読んだリスト状のデータに基づいて、リストを生成する。簡単だ。しかもLISPの場合、それをいちいち外部のプリプロセッサとして書かなくても、マクロという形で提供されている。
さらに、LISPにはシンボルオブジェクトがある。Rubyでは文字列やシンボルであったものを、識別子のように書くことで書きやすさ・元の文法との統一感は加速した。Rubyはこれができないのが不満だよね。Rubyにもあればいいのに、S式。
*1:インデントの考慮? 文字列の中にパターンがあった場合の考慮? ねえよそんなもん。