こんにちは、技術部の深谷(@fukajun)です。
先日(2020年10月3日)行われたKaigi on Railsにて、「Rubyで書かれたソースコードを読む技術」というタイトルで発表しました。
発表した内容について
仕事やプライベートではメインのRubyも含めていくつかの言語にふれていますが、Rubyで書かれたコードは非常に読みやすいと日々感じています。
基本的には、Ruby/Railsのコードは複雑なロジックでもすんなり理解できることが多いです。
とはいえ、なかなか動きがわからないコードに出会うこともあります。
今回は、そういった場合に自分がやっていることを中心に紹介しました。
発表資料
当日、使用した発表資料です。
発表動画
Rubyで書かれたソースコードを読む技術 / fukajun
発表に含めることができなかった内容について
ここからは、時間の都合で本編に盛り込めなかった内容を紹介します。
gemのinstallされているディレクトリを一発で開く
1つ目は、特定のgemのソースコードを読むときに便利な方法についてです。
発表では、「関連するgemのソースコードを検索しやすくする方法」や「メソッドの定義場所を探す方法」について話しましたが、「特定のgemのディレクトリを開く方法」については触れませんでした。
これについてはbundler
またはgem
のopen
コマンドを利用することで、gemがインストールされているディレクトリをエディターでサッと開けます。
以下の例では、activesupport
gemのディレクトリを開いています。
$ bundle open activesupport
単にディレクトリパスを表示する方法もあります。
$ bundle info activesupport > * activesupport (6.0.3.3) > Summary: A toolkit of support libraries and Ruby core extensions extracted from the Rails framework. > Homepage: https://rubyonrails.org > Path: /Users/fukajun/.rbenv/versions/2.7.0/lib/ruby/gems/2.7.0/gems/activesupport-6.0.3.3
ちなみに、以前この目的で利用していたbundle show
はDEPRECATEDになったようです。
繰り返し実行されるコードの特定の実行状態を知りたい
2つ目は、繰り返し実行されるコードの特定の実行状態をみる方法についてです。
発表では、具体的な動作を知るためにbyebug
,binding.irb
などのデバッガを利用する話をしました。
動かしてみて1,2回実行されるコードであれば扱いやすいですが、
繰り返し実行されるコードでは、その行で何度も止まってしまい特定の実行状態まで進めるのは大変です。
そんなときは、非常にシンプルですがif/unless
と組み合わせて特定の条件のみデバッガが起動するようにすると自分が知りたい実行状態をみることができます。
100.times do |i| byebug # 何回も止まってしまうのですすめるのが大変 end # 適用後 100.times do |i| byebug if i == 42 end
便利にスタックトレースをたどる
3つ目は、もうすこし楽にスタックトレースにあるコードをたどる方法についてです。
発表のなかで、caller
やbacktrace
でスタックトレースを表示し、エディターを使って順にたどっていく話をしました。
このとき資料に書いていた「byebugを利用しているときのさらに便利な読み方」には触れられなかったので、ここでもう少し詳しく説明したいと思います。
byebug
デバッグ中にbacktrace
コマンドを実行するとスタックトレースがズラッと表示されます。
(byebug) backtrace --> #0 AlbumsController.index at /Users/fukajun/reading-code/photos/app/controllers/albums_controller.rb:4 #1 ActionController::BasicImplicitRender.send_action(method#String, *args#Array) at /Users/fukajun/.rbenv/versions/2.7.0/lib/ruby/gems/2.7.0/gems/actionpack-6.0.3.3/lib/action_controller/metal/basic_implicit_render.rb:6 #2 AbstractController::Base.process_action(method_name#String, *args#Array) at /Users/fukajun/.rbenv/versions/2.7.0/lib/ruby/gems/2.7.0/gems/actionpack-6.0.3.3/lib/abstract_controller/base.rb:195 ... 以下省略 ...
このスタックトレースの階層をup
コマンドを利用するとbyebug
で止めている位置から順に上ることができます。(逆にdown
では下ることができます)
下記の例では2回up
を実行して、2階層上っています。
各実行結果に、次の階層の呼びだし行を中心にソースコードが表示されます。
エディターでコードを開くことなく、スタックトレースを追えるのでかなり楽に読めるようになります。
もう少し、表示行数がほしいときはset listsize <表示する行数>
で増やせます。
さらに、該当のファイル全体を見たくなった場合はeditor
コマンドを実行し
vimなどのエディターで該当ファイルを開くこともできます。
[1, 5] in /Users/fukajun/reading-code/photos/app/controllers/albums_controller.rb 1: class AlbumsController < ApplicationController 2: def index 3: byebug => 4: end 5: end (byebug) up [4, 8] in /Users/fukajun/.rbenv/versions/2.7.0/lib/ruby/gems/2.7.0/gems/actionpack-6.0.3.3/lib/action_controller/metal/basic_implicit_render.rb 4: module BasicImplicitRender # :nodoc: 5: def send_action(method, *args) => 6: super.tap { default_render unless performed? } 7: end 8: (byebug) up [193, 197] in /Users/fukajun/.rbenv/versions/2.7.0/lib/ruby/gems/2.7.0/gems/actionpack-6.0.3.3/lib/abstract_controller/base.rb 193: # which is *not* necessarily the same as the action name. 194: def process_action(method_name, *args) => 195: send_action(method_name, *args) 196: end 197: (byebug) editor # abstract_controller/base.rbの195行目をvim等で開いてくれます
set listsize 20
を実行し表示行数を増やしてみました。
下方にあるsend_action
がsend
のalias
として定義されている行まで読めるようになりました。
(byebug) set listsize 20 [185, 204] in /Users/fukajun/.rbenv/versions/2.7.0/lib/ruby/gems/2.7.0/gems/actionpack-6.0.3.3/lib/abstract_controller/base.rb 185: self.class.action_methods.include?(name) 186: end 187: 188: # Call the action. Override this in a subclass to modify the 189: # behavior around processing an action. This, and not #process, 190: # is the intended way to override action dispatching. 191: # 192: # Notice that the first argument is the method to be dispatched 193: # which is *not* necessarily the same as the action name. 194: def process_action(method_name, *args) => 195: send_action(method_name, *args) 196: end 197: 198: # Actually call the method associated with the action. Override 199: # this method if you wish to change how action methods are called, 200: # not to add additional behavior around it. For example, you would 201: # override #send_action if you want to inject arguments into the 202: # method. 203: alias send_action send 204:
各階層に移動したときは、そのスコープの変数をvar i
やvar l
でみることもできます。
ちなみに、表示される変数の状態は、その階層のときの状態ではなく、今現在における変数の状態であることを覚えておくと混乱しなくて済みそうです。
デバッガーを利用してコードを読むのは少し手間ですが、
エディターで何度も開き直すことなくコードをプレビューしながらスタックトレースを追えるのであれば、活躍する場面も増えるのではないでしょうか?
まとめ
登壇をきっかけに自分がいつもどのようにRubyのコードを読んでいるか振り返ることができました。
また、もうすこし便利な方法はないか調べるきっかけにもなってよかったです。
わりと「初学者向けの内容」ということで、参加者の求める内容になっていたかは気になるところではありますが、きっと誰かの役に立てたのではないかと信じています。
今後もなにか機会をみつけて、登壇・ブログなど継続的なアウトプットをしていきたいと思います。
最後になりますが、登壇資料のレビューをしてくれた弊社エンジニアメンバーと、Kaigi on Railsイベントスタッフの方々に感謝したいと思います。