そういうこともある

エンジニア的な何かの、プログラミングとかイベントレポートとか読書感想文

Railsのソースコード読んでみる | ActiveSupport blank?編

f:id:sktktk1230:20171212153754p:plain

普段仕事で使っているRuby on Railsですが、ソースコードを読む機会もなかなかないので、試しにやってみることにしました

読めるようにするまで

1. Githubのrails/railsリポジトリから任意のフォルダにクローンする

  1. ブラウザで https://github.com/rails/rails を開く
  2. Clone or Downloadをクリック f:id:sktktk1230:20171212153907p:plain

  3. クリップボードアイコンをクリック f:id:sktktk1230:20171212153931p:plain

  4. 任意のディレクトリ配下で $ git clone https://github.com/rails/rails.git rails

2. 任意のエディタで閲覧

僕の場合はRubyMineを使ってます
※以下はRubyMineの場合の設定方法です

  1. Rubymineを開き、openを選択 f:id:sktktk1230:20171212154018p:plain

  2. 先程cloneした任意のフォルダを選択する

  3. 下記のように取り込めている f:id:sktktk1230:20171212154031p:plain

読んだ箇所

Rails書いているとよく使う blank? を今日は読んでみようと思います

どんな使い方だっけ?

読んで見る前に使い方を調べてみます Railsの日本語ドキュメントを見てみると

ruby 変数.blank?
nil? + empty? のようなメソッド。nilまたは空のオブジェクトをチェックできる。
nil, "", " "(半角スペースのみ), , {}(空のハッシュ) のときにfalseを返します。
Railsで拡張されたメソッドで、Rubyのみでは使えないのでご注意ください。
引用:Railsドキュメント

ざっくり値がないものを確認したいときとかに便利ですね。ruby標準でも欲しいなと思ったりします

ソースコードを読んでみる

1. railsプロジェクトのactivesupportにある機能なので、activesupportディレクトリのlib配下で def blank? を探してみます

f:id:sktktk1230:20171212154210p:plain

2. 該当箇所が10個ほどあったので、それぞれみてみます

1. activesupport > lib > active_support > core_ext > date > blank.rb
# frozen_string_literal: true

require "date"

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

Dateクラスに対してblank?をすると必ずfalseが返り値のようです
オープンクラスで実装しているようです

オープンクラスとは?

既存するクラスを好きな場所で再オープンし、メソッド修正・追加など任意の変更を加えられる機能のこと。
引用: [Ruby] メタプログラミングの入り口、オープンクラスを理解する

2. activesupport > lib > active_support > time_with_zone.rb
中略

# An instance of ActiveSupport::TimeWithZone is never blank
def blank?
  false
end

中略

ActiveSupport::TimeWithZoneの場合も同じように必ずfalseが返り値のようです

3. activesupport > lib > active_support > core_ext > date_time > blank.rb
# frozen_string_literal: true

require "date"

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

DateTimeクラスに対してblank?をすると必ずfalseが返り値のようです

4. activesupport > lib > active_support > core_ext > object > blank.rb
Object.blank?
# An object is blank if it's false, empty, or a whitespace string.
# For example, +false+, '', '   ', +nil+, [], and {} are all blank.
#
# This simplifies
#
#   !address || address.empty?
#
# to
#
#   address.blank?
#
# @return [true, false]
def blank?
  respond_to?(:empty?) ? !!empty? : !self
end

respond_to?の使い方を調べてみると

respond_to?メソッドは、レシーバのオブジェクトに対してメソッドを呼び出せるかどうかを調べます。引数nameにはメソッド名をシンボルか文字列で指定します。メソッドnameを持っていればtrue、なければfalseが返ります。
レシーバのクラスのメソッドだけでなく、親クラスやインクルードしているモジュールのメソッドも対象になります。デフォルトではpublicなメソッドとprotectedなメソッドを調べますが、第2引数にtrueを指定するとprivateなメソッドも含めて調べます。
引用:Rubyリファレンス:respond_to? (Object)

empty?が実装されているかチェックしているようです
そしてtrueの場合は!!empty? で falseの場合は !selfが返ります

なぜempty?の前に!!が付いているのかわからなかったので、更に調べてみます

該当箇所のコミットログを見てみると f:id:sktktk1230:20171212160213p:plain

126dc47で変更しているようです
Githubで見てみると

github.com

コントリビューターから質問されているところを見つけました f:id:sktktk1230:20171212160539p:plain

rubyという言語仕様上empty?が書き換えられて必ず TrueClass / FalseClass のシングルトンが返り値となるわけではない為、こういう書き方をしているようです

!self はObjectクラスのインスタンスに対して行っているので、基本的にfalseが返り値となります
※ trueになるケースが思いつかなかったので、誰か知っている方いらっしゃいましたら教えてください

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

Dateクラスのときと同じようにオープンクラスしてblank?メソッドを追加しているようです
下記クラスも同様ですね

FalseClass
class FalseClass
  # +false+ is blank:
  #
  #   false.blank? # => true
  #
  # @return [true]
  def blank?
    true
  end
end
TrueClass
class TrueClass
  # +true+ is not blank:
  #
  #   true.blank? # => false
  #
  # @return [false]
  def blank?
    false
  end
end
String
BLANK_RE = /\A[[:space:]]*\z/

# 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? || BLANK_RE.match?(self)
end

レシーバがempty?またはBLANK_REにマッチする場合はtrueのようです
BLANK_REをみてみると BLANK_RE = /\A[[:space:]]*\z/
[:space:]はPOSIX文字クラスというものです。スペースとタブと改ページにマッチします

※以前の記事でPOSIX文字クラスについて調べています

上記2パターンのどちらかにマッチした場合にtrueが返り値になるようです

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

Numericの場合も必ずfalseが返り値のようです
0だともしかしたらtrueが返り値かもとか思ってしまいそうなので、確認するのは大事ですね

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

読んでみて

ソースコードを読むのは結構億劫だったりしますが、よく使う処理などは意外な発見があったりして楽しめそうです