箱のプログラミング日記。

えんじにあ奮闘記

gemのコードリーディングを初めてやってみる【ActiveSupport】

f:id:y_hakoiri:20191120232646j:plain

タイトルの通り、gemのコードリーディングを初めてやってみたので手順などメモ。

なぜやるのか

最近gemのコードを読む機会が多くて、大体はREADMEでは分からない情報を取ってきたい時にGitHubリポジトリ内で検索かけつつ読むくらいだったのだけど、

他人のコードを読むことで明らかに自分の実装レベルも上がったことを感じているので、本格的にやってみたいと思った。

参考にした記事

下記の記事を参考にさせていただきました。ありがとうございます。

RubyGemコードリーディングのすすめ

ソースコード・リーディングしよう![GemJam][ActiveSupport] | 酒と涙とRubyとRailsと

これらの記事でも、コードリーディングが実装力上達に効果的である旨が書いてあります。わくわく!

対象のgem

今回はRailsのActiveSupportにします。

Active Support コア拡張機能 - Railsガイド

最初から難易度の高いgemを選ぶと挫折しそうなので、まずは普段自分がよく使う馴染みのあるところから入ろうかなと。

やること

  • gemをインストール
  • 読みたいファイルを探す
  • デバッグ

gemをインストール

active_supportというディレクトリを作りました。

$ mkdir active_support
$ cd active_support
$ bundle init

Gemfile

# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem 'activesupport'
gem 'pry'

デバッグ用にpryも入れます。

$ bundle install --path vendor bundle

Gemfile.lock

GEM
  remote: https://rubygems.org/
  specs:
    activesupport (6.1.4.1)
      concurrent-ruby (~> 1.0, >= 1.0.2)
      i18n (>= 1.6, < 2)
      minitest (>= 5.1)
      tzinfo (~> 2.0)
      zeitwerk (~> 2.3)
    coderay (1.1.3)
    concurrent-ruby (1.1.9)
    i18n (1.8.11)
      concurrent-ruby (~> 1.0)
    method_source (1.0.0)
    minitest (5.14.4)
    pry (0.14.1)
      coderay (~> 1.1)
      method_source (~> 1.0)
    tzinfo (2.0.4)
      concurrent-ruby (~> 1.0)
    zeitwerk (2.5.1)

PLATFORMS
  ruby

DEPENDENCIES
  activesupport
  pry

BUNDLED WITH
   2.1.4

準備完了。

読みたいファイルを探す

今回はコア拡張機能の中からblank?のコードを読んでみたい。

ここにあります。

クラスごとにblank?メソッドが定義されていることがわかる。

class NilClass
  # +nil+ is blank:
  #
  #   nil.blank? # => true
  #
  # @return [true]
  def blank?
    true
  end
end

class FalseClass
  # +false+ is blank:
  #
  #   false.blank? # => true
  #
  # @return [true]
  def blank?
    true
  end
end

nilやfalseがレシーバの時は毎回trueが返る。

@return [true]がメソッドの返り値を表すコメントの模様。

class TrueClass
  # +true+ is not blank:
  #
  #   true.blank? # => false
  #
  # @return [false]
  def blank?
    false
  end
end

# 〜中略〜

class Numeric #:nodoc:
  # No number is blank:
  #
  #   1.blank? # => false
  #   0.blank? # => false
  #
  # @return [false]
  def blank?
    false
  end
end

class Time #:nodoc:
  # No Time is blank:
  #
  #   Time.now.blank? # => false
  #
  # @return [false]
  def blank?
    false
  end
end

trueやNumericオブジェクト、Timeオブジェクトがレシーバの時は常にfalseを返す。これらはblankではない。

class Array
  # An array is blank if it's empty:
  #
  #   [].blank?      # => true
  #   [1,2,3].blank? # => false
  #
  # @return [true, false]
  alias_method :blank?, :empty?
end

class Hash
  # A hash is blank if it's empty:
  #
  #   {}.blank?                # => true
  #   { key: 'value' }.blank?  # => false
  #
  # @return [true, false]
  alias_method :blank?, :empty?
end

Array,Hashはempty?と挙動が同じためエイリアスが張られているだけ。

class String
  BLANK_RE = /\A[[:space:]]*\z/
  ENCODED_BLANKS = Concurrent::Map.new do |h, enc|
    h[enc] = Regexp.new(BLANK_RE.source.encode(enc), BLANK_RE.options | Regexp::FIXEDENCODING)
  end

  # A string is blank if it's empty or contains whitespaces only:
  #
  #   ''.blank?       # => true
  #   '   '.blank?    # => true
  #   "\t\n\r".blank? # => true
  #   ' blah '.blank? # => false
  #
  # Unicode whitespace is supported:
  #
  #   "\u00a0".blank? # => true
  #
  # @return [true, false]
  def blank?
    # The regexp that matches blank strings is expensive. For the case of empty
    # strings we can speed up this method (~3.5x) with an empty? call. The
    # penalty for the rest of strings is marginal.
    empty? ||
      begin
        BLANK_RE.match?(self)
      rescue Encoding::CompatibilityError
        ENCODED_BLANKS[self.encoding].match?(self)
      end
  end
end

Stringは何やらゴニョゴニョやってるなー。長くなりそうなので後回し。

面白いなーと思ったのが、Objectクラスにpresent?presenceも定義されていた。blank.rbに君たちも居たのね。

class Object
  # @return [true, false]
  def blank?
    respond_to?(:empty?) ? !!empty? : !self
  end

  # An object is present if it's not blank.
  # @return [true, false]
  def present?
    !blank?
  end

  # Returns the receiver if it's present otherwise returns +nil+.
  # @return [Object]
  def presence
    self if present?
  end
end

blank?についてもうちょっと見てみると、

class Object
  # @return [true, false]
  def blank?
    respond_to?(:empty?) ? !!empty? : !self
  end
  # 〜中略〜
end

レシーバがempty?メソッドを受け取れる場合は!!empty?を、そうでない場合は!selfを返す実装。

!selfから先にいくと、Rubyではnilとfalse以外は全て真なので、

  • selfがnilもしくはfalseの場合...trueを返す(つまりブランクである)
  • selfがnilもしくはfalseでない場合...falseを返す(つまりブランクではない)

ということになる。

class NilClass
  # +nil+ is blank:
  #
  #   nil.blank? # => true
  #
  # @return [true]
  def blank?
    true
  end
end

class FalseClass
  # +false+ is blank:
  #
  #   false.blank? # => true
  #
  # @return [true]
  def blank?
    true
  end
end

最初のここと一致する。

実際はレシーバがnilの場合はNilClass.blank?が、falseの場合はFalseClass.blank?が呼ばれるので、オーバーライドしたりしない限りはObject.blank?のselfにnilかfalseが入ることはなさそう。

で、もう一辺の!!empty?

(「!!」...?)

二重感嘆符は元々論理値を返さないメソッドの返り値をtrueかfalseに統一したい場合に使用するらしい。

empty?の返り値は元々trueかfalseだから実際にはempty?と同じ動きかな。

まとめると、

class Object
  # @return [true, false]
  def blank?
    respond_to?(:empty?) ? !!empty? : !self
  end
  # 〜中略〜
end
  • レシーバがempty?メソッドを受け取れる時はempty?の結果を返す
  • レシーバがempty?メソッドを受け取れない時は
    • レシーバがnilもしくはfalseのときはtrueを返す
    • レシーバがnilもしくはfalse以外のときはfalseを返す

となる。

デバッグする

Object.blank?の挙動を確かめたいのでこんな実行ファイルを作ってみた。

sample.rb

require 'pry'
require 'active_support/all'

def main
  p StandardError.new.blank?
end

main

StandardError < Exception < Object

なので、Object.blank?が呼ばれるはず。

さっきの処理のところにbinding.pryを挟んで

class Object
  def blank?
    binding.pry
    respond_to?(:empty?) ? !!empty? : !self
  end
  # 〜中略〜
end

いざ実行

$ bundle exec ruby sample.rb

    18: def blank?
 => 19:   binding.pry
    20:   respond_to?(:empty?) ? !!empty? : !self
    21: end

[1] pry(#<StandardError>)> respond_to?(:empty?)
=> false
[2] pry(#<StandardError>)>
[3] pry(#<StandardError>)>
[4] pry(#<StandardError>)> self
=> #<StandardError: StandardError>
[5] pry(#<StandardError>)>
[6] pry(#<StandardError>)> !self
=> false
[7] pry(#<StandardError>)>
false

ちゃんとfalseが返ってきた。

String.blank?についてはまた次回...

参考

rails/blank.rb at main · rails/rails · GitHub

RubyGemコードリーディングのすすめ

ソースコード・リーディングしよう![GemJam][ActiveSupport] | 酒と涙とRubyとRailsと

Ruby でびっくりマークが2つ出てくるアレの意味 - Just do IT