はじめに
最近業務でRailsを触り始めました。
Rails tutorialはこなしているものの、仕事の対象は既存の大規模アプリケーションなので、カスタマイズされている部分が多く読み解くのに苦労しています。
そもそもなぜコードを読むのに苦労するのかということについて、おおよそ以下の3つの項目が重なっているためだということがわかったので、Rakeを例にとって説明します。
Rubyを読む際に苦労する点
- 豊富なシンタックスシュガー
- 定義はどこに
- gemの仕様
Rakeタスク
私が触っているアプリケーションにはこのようなタスクが定義されていました。
namespace :hoge do
desc 'Task without arguments'
task task1: :environment do
puts "This is task1"
end
desc 'Task with arguments'
task :task2, ['arg1', 'arg2'] => :environment do |arg1, arg2|
puts "This is task2 with: #{arg1}, #{arg2}"
end
end
Rakeタスク
私が触っているアプリケーションにはこのようなタスクが定義されていました。
namespace :hoge do
desc 'Task without arguments'
task task1: :environment do # :environment とは
puts "This is task1"
end
desc 'Task with arguments'
# 唐突に登場する "=>" (Hashの初期化に使用する記号では?
# 上のタスク定義とフォーマットが異なる。こちらでは :environment が "=>" で指し示されているけれど何が違うのか?
task :task2, ['arg1', 'arg2'] => :environment do |arg1, arg2|
puts "This is task2 with: #{arg1}, #{arg2}"
end
end
🤔
シンタックスシュガーを外す
- 豊富なRubyシンタックスシュガー → シンタックスシュガーを外した場合を考える
- 定義はどこに
- gemの仕様
元々の定義
task :task1 => :environment do
...
end
task :task2, ['arg1', 'arg2'] => :environment do |arg1, arg2|
...
end
引数のシンタックスシュガーを外す
task { :task1 => :environment } do
...
end
task :task2, { ['arg1', 'arg2'] => :environment } do |arg1, arg2|
...
end
メソッド呼び出しのシンタックスシュガーを外す
task({ :task1 => :environment }) do
...
end
task(:task2, { ['arg1', 'arg2'] => :environment }) do |arg1, arg2|
...
end
ここまでくると、 task()
メソッドにタスク名その他の引数と、ひとつのブロックを与えて呼び出している、ということがわかります。
(引数ブロックについてはこのポストがわかりやすい)
引数のフォーマットが異なる 🤔
しかし、ブロックに引数がある場合とない場合で、 task()
メソッドの引数のフォーマットが異なり、ここはまだよくわかりません。
# 引数はHashのみ
# タスク名っぽいsymbolがHashのkeyに入ってる
task({ :task1 => :environment }) do
...
end
# タスク名っぽいsymbolとHash、ふたつの値を引数にとっている
task(:task2, { ['arg1', 'arg2'] => :environment }) do |arg1, arg2|
...
end
定義を辿る
- 豊富なRubyシンタックスシュガー → シンタックスシュガーを外した場合を考える
- 定義はどこに → RubyMineのGo To Declarationを使用する
- gemの仕様
RubyMine
選択した要素を右クリックして Go To -> Declaration で定義に飛んでくれます。
namespaceやModuleをincludeしている部分も解決してくれるので優秀です。
task()
の実装を追っていくと、最終的に Rake::TaskManager.define_task()
がタスク生成処理を行っているということが分かります。
gemの実装を読む
- 豊富なRubyシンタックスシュガー → シンタックスシュガーを外した場合を考える
- 定義はどこに → RubyMineのGo To Declarationを使用する
- gemの仕様 → コードを読む
Rake::TaskManager.define_task()
がタスク生成処理を司っているので、見てみます
def define_task(task_class, *args, &block) # :nodoc:
task_name, arg_names, deps = resolve_args(args) # 引数の解釈
...
task_name = task_class.scope_name(@scope, task_name)
deps = [deps] unless deps.respond_to?(:to_ary)
deps = deps.map { |d| Rake.from_pathname(d).to_s }
task = intern(task_class, task_name) # task_class.new してオブジェクト生成
# 以下taskオブジェクトのセットアップ
task.set_arg_names(arg_names) unless arg_names.empty?
...
task.enhance(deps, &block)
...
end
ここまでくるとコードを読むことができるようになり、↑の例だと resolve_args()
の内容を辿ると引数の解釈部分の実装が出てきます。
定義からわかったtaskの引数
一行一行解説はしませんが、 resolve_args()
を読んでいくと以下のようなフォーマットになっていることが分かりました
# dependency ( `:environment` など )がない場合
task(:task_name, :arg1_name, :arg2_name, ...) do |arg1, arg2, ...|
...
end
# dependencyがあり、ブロックに引数がない場合
task({ :task_name => [ dependency1, dependency2, ...] }) do
...
end
# dependencyがあり、ブロックに引数がある場合
task(:task_name, { [:arg1_name, :arg2_name, ...] => [ dependency1, dependency2, ...] }) do |arg1, arg2, ...|
...
end
# dependencyがひとつのみの場合は配列を外すことも可能
task({ :task_name => dependency }) do
...
end
まとめ
ソースを読むために:
- シンタックスシュガーを外す
- メソッド定義を突き止める
- その上で実装を読み解く