ちなみに

火曜日の空は僕を押しつぶした。

第61回 Ruby/Rails勉強会@関西 で DSL について話してきた

参加

二年ぶりくらいに Ruby/Rails勉強会@関西 に参加してきた。61回目ということでどうせなら前回の60回目に行くべきだったと変な悔しさを覚えた。

http://rubykansai.doorkeeper.jp/events/10392

久しぶりの初級者レッスン

当日は僕にしては珍しく40分前には最寄り駅について、お昼ごはんまで食べて意識高い感じだった。勉強会のときはだいたい面倒くさくてお昼を抜くことが多い。

しかし @higaki さんのつぶやきを見ていたにも関わらず、残念なことに南側から出てしまっていて、見てきていた地図と全然違うし、歩いても全然それっぽい感じにならないしでテンパる。結局30分歩き続けて汗だくになって最初の場所に戻ると、北側にも東口があることに気付いてそちらから出たらすぐに辿りつけた。10分くらい遅刻してしまう。

やっと着いた

勉強会については必死でスライドを作っていてあまり発表を聞けなかった。スライドは当日の5時くらいから作っていて発表者としての自覚が足りなかったと思う。

初級者レッスンではグループになった人の大半が Ruby を始めたばかりという感じの人で、しかも仕事で使うという感じの人も結構いた。以前は趣味でという方が多かったのでだいぶ浸透してきたなあという印象があった。

懇親会

ビールを目指して歩いてる

懇親会にも参加してきた。大企業でSIerをしていた人の話とかを聞けて勉強になった。発表についても指摘や感想をいただけて気付きがある実のある渾身会だった。 寝てなかったので後半眠くなって話を聞きながら寝てしまったのが失敗…。今度はちゃんとしたい。

初参加の人が最近勉強会流行ってるということを言われていて、またそういう流れになっているのかという発見もあった。コミュニティが元気になるのはとてもうれしい。

帰宅中、最寄りのバスを降りたら蛍がたくさん居て一日の最後までよい日だった。

発表

@htomine さんのこの天才的なアドバイスにより、背景を真っ黒にしてみたらいつもより見た目がましなスライドになってテンションが上がってしまい、気がついたら116枚になっていた。やり過ぎたと思ったけど、中身の薄い発表を早口でして時間が余ることが多かったので、分量がそれなりにあるスライドは結果的には良かったのかもしれない。

さて、僕の発表だけれど、依頼を頂いてからテーマを考えて、その時ちょうどいまの仕事向けのDSLを書いていたのでDSLにした。細かい内容についてはスライドを作り始めるまではぼんやりしていた。書き始めると、「DSLで業務を効率化する。」「DSLは簡単に書ける。」「やり過ぎると死ぬ。」という三点がメインになっていった。付け焼刃すぎてちゃんと伝わった自信はない。

時間がなくてコードをエディタの画面のキャプチャにしたら解像度が低くてプロジェクタで映すとほとんど見えなかった。コードが出るたびにとても申し訳ない感じになっていた。

今回は今までで一番緊張せずにできたと思う。ただし眠くて思考がにぶっていただけな気もする。

DSLの分類について

DSLの分類は Rubyで自然なDSLを作るコツ:値を設定するときはグループ化して代入 - ククログ(2014-02-13) に従った。

元記事では以下のように分類されている。

● 定義系:動作に名前をつける。(メソッド定義とかの特化版)
  ○ task :test:タスクを定義
  ○ get "/":「GET / HTTP/1.1」されたときの動作を定義
● 宣言系:登録する。N回実行するとN個登録できる。(attr_readerとかの特化版)
  ○ source "https://rubygems.org/":RubyGemsの取得元を宣言
  ○ gem "rake":使うRubyGemsを宣言
● 操作系:実行する。(メソッド呼び出しの特化版)
  ○ ruby("test/run-test.rb"):Rubyでスクリプトを実行
● 設定系:値を変える。N回実行すると最後の値が有効になる。(代入)
  ○ cache_control:Cache-Controlヘッダーの値を設定

懇親会でこの分類をまとめてDSL作成時の指針として公開すると有益なのではという提案をいただいた。たしかに今回のスライドを作って自分でも頭を整理できたので、もう少し掘り下げてまとめられたら良さそうに思う。もう少し整理できたらやってみたい。

これまでは自分の意見をいう自信がなくて紹介系の発表ばかりしていたが、思い切って考えていることを伝えると、違った角度からの意見をいただけてより知見が広がる。この年になってやっと実感できた。

TDD は足かせについて

TDD と書いてしまっているけれどテストファーストのことです。

DSL はたいてい外面を良くするために試行錯誤しながら裏ではめちゃくちゃ頑張ります。受け入れテストとしてDSLで書いた擬似コードを作ってそれを動かすように作るけど、その擬似コードもたいていもっと良い方に改善していく。テストを先に書いてしまったら仕様が固定されがちなので、完成してから確認と改善のためにテストを書いていくというスタイルが僕にはあっていました。

ドキュメントについて

DSLにはドキュメントが必須だと思っています。

スライドで利点として自己説明的でドキュメントが不要と書いているのは、読むときの話であって読んだだけで内容が理解できるように作るべきであるという話である。名前付け にこだわる必要があるのはそのため。

ただしコピペで済ませられるならいいけど、人間が書く必要がある場合はドキュメントは絶対にいる。 DSLはその問題だけのために書かれた言語なので、ふつうは仕様を知らないし、そこでしか使わないので覚えても意味がない。特有の書き方は読めたとしても覚えるのが大変ですぐに忘れる。作者ですら一週間後には忘れていることがある。それにべんりな機能があったとしても作者しか知らないことは往々にある。

ちゃんとドキュメントを書いておいて忘れてもいいようにしておくべきだと思う。

定義系DSLについての誤解

僕自身が消化しきれていない状態でスライドを書いたので、定義系について誤解が含まれている。

定義系DSLとは 元記事 の定義では 動作に名前をつける とある。僕のスライドの作ってみるの項目では、定義系DSL = instance_eval と書いている。これはブロック内をDSLとして簡潔に書けるようにための実装の詳細の話であって、定義系DSLの本質とはズレていた。

別にただのブロックを登録する以下のようなのも立派な定義系DSLです。

Task.register :add do |a, b|
  a + b
end

Task.register :subtract do |a, b|
  a - b
end

設定系と定義系の使い分けについての質問

僕の理解がズレていたので、誤解を招いた結果の質問。

恐らくこの質問の真意は、「設定系で代入型かメソッド型のどちらを使うべきか。」ということだったと思う。

Klass.configure do |config|
  config.hoge = "piyo"
end

なのか

Klass.configure do
  hoge "piyo"
end

とするのか、ということだったと頭を整理して気付いた。

上記の意味であったとすると答えは 代入系 を使うべきで、こちらも 元記事 の言葉を借りるなら、設定するという意図と、上書き可能ということを明確にするためである。

じゃあ逆に定義系はなぜメソッド呼び出しの形にするのかというと、定義するということを明確にするためである。定義と言うくらいなので挙動や設定を意識合わせしてしまう認識。つまり上書きは考えていない。

task :install do |task|
  task.desc = "install binaries"
  task.dependencies = [:build, :test]
end

もしも Rake が上のような感じだったらなんだか意図がぼんやりしていると思う。

instance_eval の説明について

これも懇親会で指摘をいただいた。instance_evalself をレシーバーに を置き換えると書いているが、間違ってはいないけれど、DSLを書くという文脈に置いては、ブロック内でレシーバを省略できるようにするもの と説明した方が良かった。

つまりどういう事かというと、

def foo(&block)
  hoge = Hoge.new
  yield hoge
end

foo.do |hoge|
  hoge.piyo
  hoge.bar
end

から、hoge を省略するために instance_eval を使うといった方が分かり易かったということである。

def foo(&block)
  hoge = Hoge.new
  hoge.instance_eval &block
end

foo do
  piyo
  poyo
end

こっちの方がDSLっぽい。

設定系の書き方について

instance_eval かどうかみたいな議論になっていたが、たとえ設定系でも以下のように書くことも出来ると思う。

class Awesome
  class Config
    attr_accessor :hoge
  end

  def self.configure(&block)
    instance_eval &block
  end

  def self.config
    @config ||= Config.new
  end
end

Awesome.configure do
  config.hoge = "piyo"
end

p Awesome.config.hoge #=> piyo

見た目は一緒だけれど、実装が違う。この辺はケースバイケースというのと、好みの問題かもしれない。 個人的には設定系はブロック引数に対して設定していく形が好み。

undef_method について

FactoryGirl のように任意の名前でプロパティを定義できるようにするには method_missing を使うしかない。 この時ふつうのクラスで受けると Class クラスが継承しているメソッドや、include されているKernelメソッドを使うことができない。たとえば test というメソッドKernel で定義されているので使うことができない。method_missing にこないからである。

そういう時は BasicObject を使えばいいという話を懇親会で聞いてなるほどと思っていた。

class A < BasicObject
  def method_missing(meth, *args)
    # ...
  end
end

たしかに用を足せそうに見える。ただし、やってみて気付いたのだが必要なメソッドも使えなくなるのでちょっとつらかった。

BasicObject で定義されているのは以下のメソッドのみで、特に initialize が呼ばれないのがつらすぎる。

インスタンスメソッド
    ! != == __id__ __send__ equal? instance_eval instance_exec
privateメソッド
    method_missing singleton_method_added singleton_method_removed singleton_method_undefined

ということで DSL を定義する目的であれば、以下のような感じにするのが良さそう

UNPROXIED_METHODS = %w(__send__ __id__ nil? send object_id extend instance_eval initialize block_given? raise caller method)

(instance_methods + private_instance_methods).each do |method|
  undef_method(method) unless UNPROXIED_METHODS.include?(method.to_s)
end

これは FactoryGirl の この辺り から拝借した。

まとめ

DSL はうまい適用範囲をみつけたら、簡単、安心、べんりに使える魔法の言語です。 ただし、目的を見失うとすぐにカオスになるので、用法用量をよく守って業務を効率化しましょう。

お誘いいただいた結果、たくさんの知見を得ることができました。 @cuzic さん、並びにスタッフのみなさん、そして聞いてくださった参加者の皆さん、ありがとうございました。