ロコガイド テックブログ

「地域のくらしを、かしこく、たのしく」する、株式会社ロコガイドの技術部ブログです。主にトクバイ・ロコナビのサービス開発について発信しています。

「地域のくらしを、かしこく、たのしく」する、株式会社ロコガイドの技術部ブログです。
主にトクバイ・ロコナビのサービス開発について発信しています。

Kaigi on Railsに「Rubyで書かれたソースコードを読む技術」というタイトルで登壇しました

f:id:fukajun:20201002162420p:plain こんにちは、技術部の深谷(@fukajun)です。
先日(2020年10月3日)行われたKaigi on Railsにて、「Rubyで書かれたソースコードを読む技術」というタイトルで発表しました。

発表した内容について

仕事やプライベートではメインのRubyも含めていくつかの言語にふれていますが、Rubyで書かれたコードは非常に読みやすいと日々感じています。
基本的には、Ruby/Railsのコードは複雑なロジックでもすんなり理解できることが多いです。
とはいえ、なかなか動きがわからないコードに出会うこともあります。
今回は、そういった場合に自分がやっていることを中心に紹介しました。

発表資料

当日、使用した発表資料です。

当日の映像は公開され次第追加予定です。

発表に含めることができなかった内容について

ここからは、時間の都合で本編に盛り込めなかった内容を紹介します。

gemのinstallされているディレクトリを一発で開く

1つ目は、特定のgemのソースコードを読むときに便利な方法についてです。
発表では、「関連するgemのソースコードを検索しやすくする方法」や「メソッドの定義場所を探す方法」について話しましたが、「特定のgemのディレクトリを開く方法」については触れませんでした。
これについてはbundlerまたはgemopenコマンドを利用することで、gemがインストールされているディレクトリをエディターでサッと開けます。

以下の例では、activesupportgemのディレクトリを開いています。

$ 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 showDEPRECATEDになったようです。

繰り返し実行されるコードの特定の実行状態を知りたい

2つ目は、繰り返し実行されるコードの特定の実行状態をみる方法についてです。
発表では、具体的な動作を知るためにbyebug,binding.irbなどのデバッガを利用する話をしました。
動かしてみて1,2回実行されるコードであれば扱いやすいですが、
繰り返し実行されるコードでは、その行で何度も止まってしまい特定の実行状態まで進めるのは大変です。
そんなときは、非常にシンプルですがif/unlessと組み合わせて特定の条件のみデバッガが起動するようにすると自分が知りたい実行状態をみることができます。

100.times do |i|
   byebug # 何回も止まってしまうのですすめるのが大変
end

# 適用後
100.times do |i|
   byebug if i == 42
end

便利にスタックトレースをたどる

3つ目は、もうすこし楽にスタックトレースにあるコードをたどる方法についてです。
発表のなかで、callerbacktraceでスタックトレースを表示し、エディターを使って順にたどっていく話をしました。
このとき資料に書いていた「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_actionsendaliasとして定義されている行まで読めるようになりました。

(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 ivar lでみることもできます。
ちなみに、表示される変数の状態は、その階層のときの状態ではなく、今現在における変数の状態であることを覚えておくと混乱しなくて済みそうです。

デバッガーを利用してコードを読むのは少し手間ですが、
エディターで何度も開き直すことなくコードをプレビューしながらスタックトレースを追えるのであれば、活躍する場面も増えるのではないでしょうか?

まとめ

登壇をきっかけに自分がいつもどのようにRubyのコードを読んでいるか振り返ることができました。
また、もうすこし便利な方法はないか調べるきっかけにもなってよかったです。
わりと「初学者向けの内容」ということで、参加者の求める内容になっていたかは気になるところではありますが、きっと誰かの役に立てたのではないかと信じています。
今後もなにか機会をみつけて、登壇・ブログなど継続的なアウトプットをしていきたいと思います。
最後になりますが、登壇資料のレビューをしてくれた弊社エンジニアメンバーと、Kaigi on Railsイベントスタッフの方々に感謝したいと思います。