本セッションの登壇者
セッション動画(YouTubeチャンネル登録もお願いします。)
よろしくお願いします。大倉です。今日は「DSLを作る時に便利なテクニック@Ruby」ということでお話をします。
スライドは#tfconハッシュタグにあげてしまいました。ご意見ご感想ありましたら、YouTubeのチャットかTwitterでお願いします。あとで見ます。
自己紹介です。大倉といいます。フリーランスの開発者をやっています。主な活動としてはKaigi on Railsというカンファレンスのチーフオーガナイザーをやっていたりとか、OSSで自作のJSONシリアライザであるAlbaというものを作っていたりします。今日の題材にもなっています。詳細はTechFeedのページをご覧ください。
Rubyといえば…
Rubyといえば、やっぱりDSLだと思うんです。DSLとはDomain Specific Languageの略です。
Rubyの世界ではRake(makeっぽいやつ)や、テスト用のRSpec、WebフレームワークのSinatraなどが知られております。
今回はこのDSLを便利に書く方法をご紹介をしていきます。
今回取り上げるDSL
今回取り上げるDSL、具体的なものはこちらです。

コードを上からざっと見ていくと、UserResourceという何も継承していない普通のクラスがあります。`Alba::Resource`をincludeしていて、このincludeの後に、このroot_keyとかattributesとかattributeとか、こういうクラスメソッドが使えるようになっています。
こういう感じのDSLのことをクラスマクロなんて呼んだりもします。マクロってRubyにはないんですけど、マクロっぽいということです。
クラスの使い方と内部の動作
今のコードはクラス定義だけだったので、次にそのクラスをどう使うかを説明します。
とくに何の変哲もないUserクラスを定義して、それを普通に初期化した後、一番下でUserResource.newにインスタンス化したuserオブジェクトを渡してあげてシリアライズします。

すると、ここではお見せしないですけど、長いJSONがバーっと吐き出されます。そのときに、たとえばさっき定義したname_with_emailだったらこういう文字列が吐かれます、みたいな感じになります。
動きの概要をユーザー目線で見るとこうです。

ユーザーがモジュールをクラスにincludeすると、クラスマクロ(さっきのattributesとか)が追加されてきます。その追加されたメソッドを呼ぶことで、クラスの内部の方に情報を貯めます。これはインスタンスではなくクラスの内部です。そしてインスタンスメソッドserializeというのを呼んであげたタイミングで、蓄積された情報と、引数に渡したuserオブジェクトの情報を足して、JSONを取るということになります。
内部の技術的な話をすると、includedっていうフックがRubyにはあって、モジュールをincludeすると、普通はインスタンスメソッドが追加されるんですが、ちょっと細工をするとクラスメソッドも同時に追加できます。
さらにクラスインスタンス変数を設定します。Rubyの世界ではクラスもオブジェクトですので、クラスインスタンス変数は「クラスをインスタンスとして扱い、インスタンス変数を持つ」ということです。

さっきのattributesとかattributeとかを呼んでいくと、そのインスタンス変数をどんどんどんどん蓄積していきます。initializeメソッド内でインスタンス化(new)するタイミングで、自身のクラスから情報を取ってきて、インスタンスに情報を引き渡します。
DSLの課題
課題はいくつかあります。
DSLが成長すると、クラスインスタンス変数がすごく多くなってくるんですよね。初期値もたくさんの種類をとります。nilだったり空配列だったり空ハッシュだったり空文字かもしれません。いろいろあります。
さらにこれはクラスインスタンス変数なので、クラスが継承されると自動的には引き継がれないのでけっこうややこしいです。
このクラスインスタンス変数というのが今回の話の肝です。includeされたタイミングで設定され、初期化のタイミング(newが呼ばれたタイミング)でコピーされ、継承のタイミングでもコピーされなきゃいけません。

解決策
ということで大変なんですけど、解決策としてはこういう感じのコードになります。文字の大きさ大丈夫ですかね?

2行目のDSLSが、クラスインスタンス変数の名前と初期値を一括で管理するための定数になっています。それを各種のフックの中で呼ぶという構造になっています。
includedの中では、baseはその大元のクラスです。そのクラスの文脈の中でDSLをループで回して、すでにその名前の変数がなければセットしています。このときinitial.dupっていうのが後で出てきます。
これは普通のinitializeメソッドで、これも基本的には同じ感じです。

instance_variable_set(名前)でインスタンス変数をセットしますが、その値は親クラスのメソッドを名前で呼んでいます。__send__を使います。
呼ばれる側のクラスの方は、attr_readerにkeysで名前だけを渡して、いわゆるreaderメソッド、getterメソッドを定義したりとか、inheritedでサブクラスに対してインスタンス変数をセットするみたいなことをしてます。

まとめ
要は「定数を使うとすごい便利だよ」という話です。
あと定数ってメモリ領域上1個しかないので、cloneとかdupしないと、同じものをいじった人がいろいろと大変になり、バグになるので気をつけましょう。

あとはループの中でinstance_variable_setをするっていうのは結構便利なので、使ってみていいんじゃないかなと思います。
includedフックとinheritedフックがあって、組み合わせることでいろんな継承とかインスタンス化とかに対応できる、使い勝手のよいDSLが提供できるでしょう。

ではみなさんもRubyで楽しいDSLライフをどうぞ。
資料はこちらです。ありがとうございました。
書き起こしが公開されました!RubyでDSLを書く人にぜひ読んでほしいです、お役立てください!