タイトルの通り、gemのコードリーディングを初めてやってみたので手順などメモ。
なぜやるのか
最近gemのコードを読む機会が多くて、大体はREADMEでは分からない情報を取ってきたい時にGitHubリポジトリ内で検索かけつつ読むくらいだったのだけど、
他人のコードを読むことで明らかに自分の実装レベルも上がったことを感じているので、本格的にやってみたいと思った。
参考にした記事
下記の記事を参考にさせていただきました。ありがとうございます。
ソースコード・リーディングしよう![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