RubyでDSLっぽいことをする時の基本のような何か
例によって自分用のメモ書きなのですが、
強気なタイトルに出られないのは、自分の中で確信が持てないからで・・・
とはいえ、実際に仕事で使っているテクニックですし、
使おうとするたびに毎回調べたり検証コードを書いたりが面倒なので、
一度まとめておきます...φ(・ω・`)
「DSL的に書く」とは?
先日の「Gunma.web」*1でもRubyに対して「黒魔術」という表現を使いましたが、
例えばこんなDSL的なコードで細かい挙動を変えられる、というのはその一端です
class User < ActiveRecord::Base validates :user_id, :presence => true # 厳密に書こうとするとこうなる validates(:user_name, {:presence => true}) end
この場合の「validates」は単なるクラスメソッドで、
引数の「()」が省略でき、最後の引数がHashの場合は「{}」を省略できるという、
Rubyのメソッド呼び出しの仕様を利用しているだけです
でも、「それだけのこと」で、高い表現力を実現しているのが、
Rubyのすごいところではありますが、それはさておいて・・・
逆に、このようなDSL的な表現を自分で書くにはどうしたらいいのでしょう(´・ω・)?
つまり、最終的にこんなのが書きたいわけです
class Reply < Comment comment_type :reply # これをキーにしてクラスの挙動を変える end
この場合、Replyの親クラスであるCommentクラスの実装が問題になります
言い換えると、以下をどう実装するか、ということです
class Comment class << self # クラスメソッド def comment_type(t) # 何らかの実装 end end # インスタンスメソッド def view_comment_type # 何らかの実装 end end
「comment_type」というメソッドは「クラスの定義」を表しているので、
(インスタンスではなく)クラス自身がその情報をどう抱えるか、
その手法がポイントになります
とりあえずクラス変数を使ってみる
Rubyのクラス変数には罠が多くて、
覚えたての頃は多用してましたが、今は一切使ってませんΣ(・ω・ノ)ノ
とはいえ、「クラス自身が情報を持つ」という言葉から、
最初に思いつくのはクラス変数でしょう
というわけで、まずクラス変数で書いてみます(´・ω・)っ
class Comment class << self def comment_type(t) @@comment_type = t end end def view_comment_type @@comment_type end end class Reply < Comment comment_type :reply end rep = Reply.new p rep.view_comment_type #=> :reply
おお、要件を満たしていますヽ(`・ω・´)ノ
しかし、こうしたらどうでしょう?
# (前略) p rep.view_comment_type #=> :reply class Tweet < Comment comment_type :tweet end tw = Tweet.new p tw.view_comment_type #=> :tweet p rep.view_comment_type #=> :tweet
なんと、後から定義したクラスによって、
元々のReplyクラスの挙動が変わってしまいましたΣ(゚Д゚)ガーン
「@@comment_type」というクラス変数は、Commentに属した単一の情報なので、
後から定義した情報で上書きされてしまうのです*2
これでは使えません
クラス自身のインスタンス変数に隠す
ところで、Rubyにおける「クラス」というのは、
「Classクラスのインスタンスを定数に結びつけたもの」です
擬似的に書けばこういうことです
class Class end rep = Class.new Reply = rep
で、「クラス」もインスタンスの一種なのであれば、
当然ながら「インスタンス変数」を定義することができます
なので、こういう書き方ができます(´・ω・)っ
class Comment class << self def comment_type(t) @comment_type = t end def view_comment_type @comment_type end end def view_comment_type # クラスメソッドのview_comment_typeに委譲 self.class.view_comment_type end end class Reply < Comment comment_type :reply end rep = Reply.new p rep.view_comment_type #=> :reply class Tweet < Comment comment_type :tweet end tw = Tweet.new p tw.view_comment_type #=> :tweet p rep.view_comment_type #=> :reply
結果的にこれでうまくいくのですが・・・わけがわからないよΣ(・ω・ノ)ノ
そもそも「class << self」の「self」ってなにを指しているのでしょう?
ちょっとコードに細工してみます
class Comment class << self def comment_type(t) p self # (1) @comment_type = t end def view_comment_type @comment_type end end def view_comment_type p self # (2) p @comment_type # (3) self.class.view_comment_type end end class Reply < Comment comment_type :reply end rep = Reply.new p rep.view_comment_type # (4) # 出力結果 Reply # (1) #<Reply:0x00000...> # (2) nil # (3) :reply # (4)
順に見ていくと、まず(1)のselfが指しているのは、
「Classクラスのインスタンスである、とあるオブジェクト」です
それを定数に代入したのが「Replyクラス」です
つまり、擬似的にはこう書いたのと同じことです
class Class def comment_type(t) @comment_type = t end def view_comment_type @comment_type end end rep1 = Class.new rep1.comment_type = :reply rep1.view_comment_type #=> :reply Reply = rep1 Reply.view_comment_type #=> :reply rep2 = Class.new rep2.comment_type = :tweet rep2.view_comment_type #=> :tweet Tweet = rep2 Tweet.view_comment_type #=> :tweet
「@comment_type」は「インスタンス変数」なので、
「インスタンスごとに値が違っていい」わけです
なので、「ReplyというインスタンスとTweetというインスタンスで値が違う」のは当然です
続いて、(2)のselfはすぐわかりますね
「Replyクラスのインスタンス」を指します
(3)は(1)とselfが違う=文脈が違うので、nilが返ります
このように、「純粋なオブジェクト指向言語」であるRubyは、
一見「黒魔術」に見える動作であっても、
実は単なる仕様の組み合わせだったりします
「self」がキーになることが多い*3ので、
わからなくなった場合は「selfが何を指しているか?」を
意識してみるといいかもしれません*4
<おまけ1>
格納場所や手順を複雑化しただけで、
「書き換えができない」わけではないことに注意
# (前略) rep1 = Reply.new p rep1.view_comment_type #=> :reply rep2 = Reply.new p rep2.view_comment_type #=> :reply rep1.class.class_eval do # 強引にインスタンス変数を書き換える @comment_type = :hoge end p rep1.view_comment_type #=> :hoge p rep2.view_comment_type #=> :hoge
クラスが抱える情報そのものを書き換えたので、
両方のオブジェクトで値が変わってしまいます
まあ、こういうところが良くも悪くもRubyなのですが(´-ω-)
<おまけ2>
定義情報を取り出すだけのメソッドがpublicなクラスメソッドにあるのは気持ち悪いし、
privateにして隠蔽したいので、私はこんな書き方をすることが多いです(´・ω・)っ
class Comment class << self def comment_type(t) @comment_type = t end end def some_method # comment_typeを使うメソッド end private def view_comment_type t = nil self.class.class_eval do # p self #=> Reply t = @comment_type end t end end
頻繁に呼び出すならこんなんとか
class Comment class << self def comment_type(t) @comment_type = t end end private def view_comment_type @comment_type ||= lambda do t = nil self.class.class_eval do # p self #=> Reply t = @comment_type end t end.call # なくても結果は同じだけど、ラストで返り値をを明示する方が好み @comment_type end end
後者は「遅延初期化と値のキャッシュ」的なテクニックなので、
いろいろな場合に使えますが、
「値が変わる場合」に使うと面倒なことに
ここまで読むような方には釈迦に説法でしょうけども(´・ω・`)
*1: http://d.hatena.ne.jp/parrot_studio/20111220/1324391867
*2: 継承が絡むとわかりづらくなる、という点で、私はクラス変数を使わないようにしています
*3: オブジェクト指向言語の大半がそうとも言う
*4: 例えば、JavaScriptで「this」が何を指しているか、と同じような感覚