Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

コードリーディングの冒険:awesome_print gem の仕組みをソースコードから探る

awesome_print は、irbpry での出力を色付きで整形し、格段に読みやすくしてくれる Ruby gem です。

たとえば、ハッシュを awesome_print で表示すると、次のようにインデントとカラーが付いた見やすい出力になります。

コードリーディングの冒険:awesome_print gem の仕組みをソースコードから探る

しかし、この美しい出力は一体どうやって実現されているのでしょうか?

「真実はコードの中にしかない。」 ― Robert C. Martin

というわけで、今回は実際にソースコードを読みながら、その仕組みを探っていきましょう。

コードリーディングの進め方

私がコードリーディングを始めるときは、まずプロジェクトの構成(ファイルやフォルダ)をざっと把握します。その後、探索の軸となる「問い」を一つ立てるのがお気に入りの方法です。

今回立てた最初の問いはこれです:

awesome_print は、どのようにして pry の出力を変えているのか?

探偵帽をかぶって、まず仮説を立ててみます:

「この gem は $stdout を差し替えて、pry の出力を横取りしたあとに整形しているのではないだろうか。」

しかし、この実装では Ruby 標準ライブラリの pretty_print のように出力をカスタマイズすることができません。また、出力を解析(パース)したり、最悪の場合コードを eval したりする必要も出てきます。これはあまり良いアイデアとは言えませんね。

そこで次に調べたのは、この gem がどのように読み込まれているかということです。エントリーポイントがわかれば、コードを読む足がかりになります。

awesome_print のロード方法

pry で awesome_print を有効にするには、次のように書きます。

require 'awesome_print'
AwesomePrint.pry!

ここで pry! がどこで定義されているのかを探してみましょう。私は Atom エディタの「ディレクトリ内検索」機能を使って見つけました。

見つかったコードがこちらです:

def pry!
  Pry.print = proc { |output, value| output.puts value.ai } if defined?(Pry)
end

どうやら Pry は、print の値を設定することで出力処理を差し替えられるようです。これで「どこから読み始めるべきか」という問いには答えが出ました 🙂

「proc」とは何かをさらに学びたい方は、「Blocks, Procs & Lambdas 完全ガイド」(Ruby Deep Dive 本のサンプル章)をお読みください。

この proc は2つの引数を受け取ります:

  • output(出力先)
  • value(評価結果の値)

そして、それぞれのオブジェクトに対してメソッドを呼び出しています。

次の問い:

この ai メソッドとは何なのでしょうか?

実は aiKernel モジュールで定義されたメソッドです。

def ai(options = {})
  ap      = AwesomePrint::Inspector.new(options)
  awesome = ap.awesome(self)

  if options[:html]
    awesome = "</pre>#{awesome}</pre>"
    awesome = awesome.html_safe if defined? ActiveSupport
  end

  awesome
end

alias :awesome_inspect :ai

Ruby のすべてのオブジェクトはデフォルトで Kernel を include しているため、どのオブジェクトに対しても ai メソッドを呼び出せるわけです。

それでは、もう少し深掘りして Inspector クラスの動作を見ていきましょう。

Inspector クラス

inspector.rb を開くと、initialize メソッドの中に巨大なオプション用ハッシュがあります。

その一部がこちら:

@options = {
  indent:        4,      # インデントに使うスペースの数
  index:         true,   # 配列のインデックスを表示する
  html:          false,  # HTML ではなく ANSI カラーコードを使う
  multiline:     true,   # 複数行で表示する
  # ...
}

その直後に続くのがこのコードです:

@formatter  = AwesomePrint::Formatter.new(self)
@indentator = AwesomePrint::Indentator.new(@options[:indent].abs)

Thread.current[AP] ||= []

ここで、フォーマット処理を担当するオブジェクトと、インデントを管理するオブジェクトの2つが生成されていることがわかります。

ところで、この Thread.current は何をしているのでしょうか?

Thread.current は現在実行中のスレッドへの参照を返します。アプリケーションで明示的にスレッドを使っていなくても、必ず「メインスレッド」が1つ存在します。

この AP 定数は、inspector.rb の先頭で次のように定義されています。

AP = :__awesome_print__

つまり何が起きているのかというと、awesome_print は Thread.current:__awesome_print__ というキーを使って、現在のスレッド内でのみ有効なデータを保存しています。これはマルチスレッド環境での問題を回避するための工夫です。

素敵なフォーマットの正体

続いて、出力の整形を行うコード、つまり AwesomePrint::Formatter クラスの中身を見てみましょう。

仕組みとしては、ai メソッドで生成された inspector オブジェクトが format メソッドを呼び出す形になっています。

def unnested(object)
  @formatter.format(object, printable(object))
end

そして Formatter クラスの format メソッドは、オブジェクトの型に応じた最適な処理方法を見つけ出し、少しのメタプログラミングを駆使して別のメソッドを呼び出します。

そのメソッドがこちら:

def format(object, type = nil)
  core_class = cast(object, type)

  awesome = if core_class != :self
    send(:"awesome_#{core_class}", object) # コアフォーマッター
  else
    awesome_self(object, type) # 見つからない場合は object.inspect にフォールバック
  end

  awesome
end

この Formatter クラスを理解するには、cast メソッドも確認しておく必要があります。

def cast(object, type)
  CORE.grep(type)[0] || :self
end

CORE 定数は、Ruby のコアクラスを表すシンボルの配列です。:self は「見つからなかった」ことを意味するシンボルです(あくまでこのコード内での話であり、Ruby 一般の意味ではありません)。

CORE = [:array, :bigdecimal, :class, :dir, :file, :hash, :method, :rational, :set, :struct, :unboundmethod]

処理の流れをまとめると:

整形対象のオブジェクトが「コアクラス」のリストに含まれていれば、専用のフォーマットが適用されます。そうでなければ、汎用的なフォーマットが使われます。

専用のフォーマッターは lib/awesome_print/formatters/ ディレクトリ以下に定義されており、ArrayHashClass などに対応しています。

たとえば、クラス用のフォーマットメソッドは次のようになっています。

def format
  superclass = klass.superclass
  if superclass
    colorize("#{klass.inspect} < #{superclass}", :class)
  else
    colorize(klass.inspect, :class)
  end
end

もちろん、自分で独自のフォーマッターを作成することも可能です。

まとめ

この記事では、配列やハッシュなどのオブジェクトを見やすい形で表示できる Awesome Print gem について、その内部実装をコードリーディングを通じて学びました。

ポイントのおさらい:

  • Pry.print に proc を設定することで pry の出力を差し替えている
  • Kernel#ai メソッドがすべてのオブジェクトから利用可能
  • Thread.current を使ってスレッドごとの状態を安全に管理
  • send によるメタプログラミングで型ごとのフォーマッターを動的に選択

楽しんでいただけたなら幸いです。新しい発見はありましたか?

ぜひお好きな SNS でこの記事をシェアして、より多くの人に届けてください 🙂

  1. VCR Gem × WebMockでRubyのテストスイートを高速化する方法

    Rubyアプリケーションが外部APIを利用しているなら、テストが遅い・APIのレート制限に引っかかるといった問題に直面したことがあるのではないでしょうか。 解決策は何があるでしょう? クライアントライブラリのHTTPメソッドを手動でスタブ化し、事前に用意したレスポンスを返す方法もあります。 しかし、これは手間がかかる上に、コードも読みにくくなってしまいます。 より良い解決策は、WebMock + VCRという強力なGemの組み合わせを使うことです。 WebMockは、主要なHTTPライブラリからのHTTPリクエストを横取り(インターセプト)します。対応しているのは例えば: net/http

  2. コードリーディングの冒険:awesome_print gem の仕組みをソースコードから探る

    awesome_print は、irb や pry での出力を色付きで整形し、格段に読みやすくしてくれる Ruby gem です。 たとえば、ハッシュを awesome_print で表示すると、次のようにインデントとカラーが付いた見やすい出力になります。 しかし、この美しい出力は一体どうやって実現されているのでしょうか? 「真実はコードの中にしかない。」 ― Robert C. Martin というわけで、今回は実際にソースコードを読みながら、その仕組みを探っていきましょう。 コードリーディングの進め方 私がコードリーディングを始めるときは、まずプロジェクトの構成(ファイルやフォルダ)をざ