そういうこともある

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

わからないこと調べてみた | rails commit log流し読み(2018/02/13)

f:id:sktktk1230:20180126163944p:plain

1. 概要

@y_yagiさんのrails commit log流し読みを読んでいてわからなかったこと調べてみました

2. 読んだエントリ

y-yagi.hatenablog.com

3. わからなかったこと

PRの中の処理に書かれていたdefined? ってなんだろう?

対象のPR

github.com

記述内容

def config_target_version
  defined?(@config_target_version) ? @config_target_version : Rails::VERSION::STRING.to_f
end

4. 調べてみた

defined?が分からなかった為、どんなメソッドなのか調べてみました

文法:
defined?

式が定義されていなければ、偽を返します。定義されていれば式の種別 を表す文字列を返します。

定義されていないメソッド、undef されたメソッド、Module#remove_method により削除されたメソッドのいずれに対しても defined? は偽を返します。

特別な用法として以下があります。

defined? yield

yield の呼び出しが可能なら真(文字列 "yield")を返します。 Kernel.#block_given? と同様にメソッドがブロック付きで呼ばれたか を判断する方法になります。

defined? super

super の実行が可能なら真(文字列 "super")を返します。

defined? a = 1
p a # => nil

引用:Rubyリファレンス2.5.0:defined?

式が定義されているかを確認するメソッドのようです
式が定義されていない場合は、nilを戻り値とします
定義されている場合の戻り値は以下となります

以下は、defined? が返す値の一覧です。

  • "super"
  • "method"
  • "yield"
  • "self"
  • "nil"
  • "true"
  • "false"
  • "assignment"
  • "local-variable"
  • "local-variable(in-block)"
  • "global-variable"
  • "instance-variable"
  • "constant"
  • "class variable"
  • "expression"

引用:Rubyリファレンス2.5.0:defined?

5. ちょっと思ったこと

こんな書き方

def config_target_version
  @config_target_version || Rails::VERSION::STRING.to_f
end

もありなのかなと思ったのですが、 defined? は式が定義されている場合のみ戻り値があるので、 少し挙動が変わってしまうので、同じ動きはしないので、これはダメそうでした

Railsのソースコード読んでみる | Active Support as_json編

f:id:sktktk1230:20171212153754p:plain

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

読めるようにするまで

以前書いた記事で読めるようにするまでの設定を画像キャプチャ付きで解説しましたので、よろしければこちらをご参照下さい
shitake4.hatenablog.com

読んだ箇所

2.11 JSON support

Active Supportが提供するto_jsonメソッドの実装は、通常json gemがRubyオブジェクトに対して提供しているto_jsonよりも優れています。その理由は、HashやOrderedHash、Process::Statusなどのクラスでは、正しいJSON表現を提供するために特別な処理が必要になるためです。
引用:RAILS GUIDES:Active Support コア拡張機能:JSON

to_json についてはこちらの記事を参照ください

shitake4.hatenablog.com

ここからは上記記事の続きになります

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

as_jsonはどんな処理をしているのか見てみます

1. activesupport > lib > active_support > core_ext > object > json.rb
1. class Object
class Object
  def as_json(options = nil) #:nodoc:
    if respond_to?(:to_hash)
      to_hash.as_json(options)
    else
      instance_values.as_json(options)
    end
  end
end

レシーバがrespond_to?(:to_hash) できる場合であれば、レシーバをハッシュにし、as_jsonしています
そうでなければ、 instance_valuesを呼んでいます

instance_variablesメソッドは、レシーバのオブジェクトが持っているインスタンス変数の名前を配列に入れて返します。
引用:Rubyリファレンス」instance_values

レシーバのインスタンス変数を持った配列に対して as_json しています

オブジェクトに対する as_json はハッシュか配列に変換して実行するということになります

2. class Hash
class Hash
  def as_json(options = nil) #:nodoc:
    # create a subset of the hash by applying :only or :except
    subset = if options
      if attrs = options[:only]
        slice(*Array(attrs))
      elsif attrs = options[:except]
        except(*Array(attrs))
      else
        self
      end
    else
      self
    end

    Hash[subset.map { |k, v| [k.to_s, options ? v.as_json(options.dup) : v.as_json] }]
  end
end
subset = if options

省略

else
  self
end

でoptionsが偽の場合はレシーバがsubsetに代入されます
真の場合は、if文内の最終評価値が、入ります

if options内をみてみます

if attrs = options[:only]
  slice(*Array(attrs))
elsif attrs = options[:except]
  except(*Array(attrs))
else
  self
end

if attrs = options[:only]でattrsに代入し、attrsが真であれ場合 slice(*Array(attrs)) が実行されます

Hash.slice はactive support の拡張機能になります

Slice a hash to include only the given keys. Returns a hash containing the given keys.

{ a: 1, b: 2, c: 3, d: 4 }.slice(:a, :b)
# => {:a=>1, :b=>2}

This is useful for limiting an options hash to valid keys before passing to a method:

def search(criteria = {})
  criteria.assert_valid_keys(:mass, :velocity, :time)
end
search(options.slice(:mass, :velocity, :time))

If you have an array of keys you want to limit to, you should splat them:

valid_keys = [:mass, :velocity, :time]
search(options.slice(*valid_keys))

引用:apidock:Ruby on Rails:slice

レシーバから引数に指定した値と一致するキーを含んだハッシュを戻り値とします

*Array(attrs) で可変長引数として配列で初期化し、引数に渡しています

この部分の処理はオプション only で指定したキーのみをレシーバから取り出す処理になります

次に elsif attrs = options[:except] を見てみます

さきほどと似ている記述です
exceptもactive supoprtの拡張機能であり、sliceとは違い引数に指定した値をレシーバから除外し、ハッシュを戻り値とする処理です

Returns a hash that includes everything but the given keys.

hash = { a: true, b: false, c: nil}
hash.except(:c) # => { a: true, b: false}
hash # => { a: true, b: false, c: nil}

This is useful for limiting a set of parameters to everything but a few known toggles:

@person.update(params[:person].except(:admin))

引用:apidock:Ruby on Rails:except

上記2パターンに当てはまらない場合は、レシーバが返ります

次に、こちらをみてみます

    Hash[subset.map { |k, v| [k.to_s, options ? v.as_json(options.dup) : v.as_json] }]

処理を順番に見てみます
Hash[] はハッシュインスタンスの作成処理です

Hashクラスのクラスメソッドは、新しいハッシュ(Hashクラスのインスタンス)を返します。の中に[キー1, 値1, キー2, 値2, ...]のようにオブジェクトを並べると、それが新しいハッシュのキーと値になります。
[]内のオブジェクトの数が奇数のときは、例外ArgumentErrorが発生します。
引用:Rubyリファレンス:Hash

subset.map { |k, v| } はsubset(さきほどのif文内で戻り値ハッシュ) に対して map でkey, valueを取り出しています

続いて [k.to_s, options ? v.as_json(options.dup) : v.as_json] を見てみます

配列の先頭にkeyをto_sしたものをいれています
配列の2番目に三項演算子 options ? v.as_json(options.dup) : v.as_json の戻り値が入ります

つまり、ここでの処理はハッシュsubsetのキー、バリューそれぞれに対して as_json し、再度ハッシュ生成しています

3. class Array
class Array
  def as_json(options = nil) #:nodoc:
    map { |v| options ? v.as_json(options.dup) : v.as_json }
  end
end

レシーバである配列の各要素に対してas_json しています

4. class Struct
class Struct #:nodoc:
  def as_json(options = nil)
    Hash[members.zip(values)].as_json(options)
  end
end

ここまでHash[members.zip(values)]見てみます
まずHash[] はハッシュの初期化処理です

membersメソッドを調べてみると、

構造体のメンバの名前(文字列)の配列を返します。

Foo = Struct.new(:foo, :bar)
p Foo.new.members  # => ["foo", "bar"]

[注意] 本メソッドの記述は Struct の下位クラスのインスタンスに対して呼び 出す事を想定しています。Struct.new は Struct の下位クラスを作成する点に 注意してください。

引用:Ruby2.5.0:Struct

zipメソッドを調べてみると

zipメソッドは、配列の要素を引数の配列other_arrayの要素と組み合わせ、配列の配列を作成して返します。transposeメソッドで[array, other_array, ...].transposeとしたときと同じく、行と列を入れ替えます。ただし、transposeメソッドと違って足りない要素はnilで埋められ、余分な要素は捨てられます。

arr1 = [1, 2, 3]
arr2 = [4, 5]
arr3 = [6, 7, 8, 9]
p arr1.zip(arr2, arr3)
 [[1, 4, 6], [2, 5, 7], [3, nil, 8]]

引用:Rubyリファレンス:zip

valuesメソッドを調べてみると

構造体のメンバの値を配列にいれて返します。

例えば以下のようにして passwd のエントリを出力できます。
require 'etc'
print Etc.getpwuid.values.join(":"), "\n"

引用:Ruby2.5.0:Struct

ここまでをまとめると、構造体からメンバー名とメンバーの値を取り出しハッシュへと変換しています
そしてそのハッシュに対し as_json しています

5. class TrueClass
class TrueClass
  def as_json(options = nil) #:nodoc:
    self
  end
end
6. class FalseClass
class FalseClass
  def as_json(options = nil) #:nodoc:
    self
  end
end
7. class NilClass
class NilClass
  def as_json(options = nil) #:nodoc:
    self
  end
end
8. class String
class String
  def as_json(options = nil) #:nodoc:
    self
  end
end
9. class Symbol
class Symbol
  def as_json(options = nil) #:nodoc:
    to_s
  end
end
10. class Numeric
class Numeric
  def as_json(options = nil) #:nodoc:
    self
  end
end
11. class Float
class Float
  # Encoding Infinity or NaN to JSON should return "null". The default returns
  # "Infinity" or "NaN" which are not valid JSON.
  def as_json(options = nil) #:nodoc:
    finite? ? self : nil
  end
end

finite? を調べてみると

数値が ∞, -∞, あるいは NaN でない場合に true を返します。 そうでない場合に false を返します。
引用:Ruby2.5.0リファレンスマニュアル:finite?

数値として問題がない場合にレシーバを返すようです

12. class BigDecimal
class BigDecimal
  # A BigDecimal would be naturally represented as a JSON number. Most libraries,
  # however, parse non-integer JSON numbers directly as floats. Clients using
  # those libraries would get in general a wrong number and no way to recover
  # other than manually inspecting the string with the JSON code itself.
  #
  # That's why a JSON string is returned. The JSON literal is not numeric, but
  # if the other end knows by contract that the data is supposed to be a
  # BigDecimal, it still has the chance to post-process the string and get the
  # real value.
  def as_json(options = nil) #:nodoc:
    finite? ? to_s : nil
  end
end

こちらは数値として問題ないか確認後文字列へ変換しています

13. class Regexp
def as_json(options = nil) #:nodoc:
  to_s
end

レシーバをそのまま文字列へ変換しています

14. module Enumerable
def as_json(options = nil) #:nodoc:
  to_a.as_json(options)
end

module Enumerable を調べてみると

繰り返しを行なうクラスのための Mix-in。このモジュールの メソッドは全て each を用いて定義されているので、インクルード するクラスには each が定義されていなければなりません。
引用:Ruby 2.5.0:Enumerable

mapやeach_with_indexなどがインスタンスメソッドとして定義されているクラスです
rubyでハッシュや配列を使う場合に、便利なメソッドが定義されています

to_a.as_json(options) ですので、レシーバを配列に変換し、as_json しています

15. class IO
class IO
  def as_json(options = nil) #:nodoc:
    to_s
  end
end

レシーバをそのまま文字列へ変換しています

16. class Range
class Range
  def as_json(options = nil) #:nodoc:
    to_s
  end
end

レシーバをそのまま文字列へ変換しています

17. class Time
class Time
  def as_json(options = nil) #:nodoc:
    if ActiveSupport::JSON::Encoding.use_standard_json_time_format
      xmlschema(ActiveSupport::JSON::Encoding.time_precision)
    else
      %(#{strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)})
    end
  end
end

まず ActiveSupport::JSON::Encoding.use_standard_json_time_format を見てみます

module ActiveSupport

  省略

  module JSON

    省略

    module Encoding #:nodoc:
      class JSONGemEncoder #:nodoc:

        省略

      end

      class << self
        # If true, use ISO 8601 format for dates and times. Otherwise, fall back
        # to the Active Support legacy format.
        attr_accessor :use_standard_json_time_format

        省略

        # Sets the precision of encoded time values.
        # Defaults to 3 (equivalent to millisecond precision)
        attr_accessor :time_precision

        省略

      end

      self.use_standard_json_time_format = true

      省略

      self.time_precision = 3

アクセッサに use_standard_json_time_format が定義されており、デフォルトでtrueに設定されています
その為、 Timeクラスの as_json 内では基本的に xmlschema(ActiveSupport::JSON::Encoding.time_precision) が実行されそうです

ActiveSupport::JSON::Encoding.time_precision も同じようにアクセッサが用意されております
デフォルト値は3です

time_precisionを調べてみると

@y_yagi氏のrails commit log流し読み(2014/07/10)で仕様変更の経緯が解説されていました y-yagi.hatenablog.com

time_precisionはミリ秒の桁数のようです ActiveSupport::JSON::Encoding.time_precision = 4に設定してみると

f:id:sktktk1230:20180206172934p:plain

ミリ秒の桁数が4桁になりました

xmlschema を調べてみると

XML Scheme (date) による書式の文字列を返します。
引用:Ruby2.5.0リファレンス:Date#xmlschema

文字列を返すメソッドのようです

次に if ActiveSupport::JSON::Encoding.use_standard_json_time_format が偽の場合を見てみます

else
  %(#{strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)})
end

まず%()を調べてみると

ダブルクオートで囲う場合と同等。
引用:Rubyで%記法(パーセント記法)を使う

ダブルクオートと同じようです

strftimeは引数のフォーマット文字列に従って、レシーバを変換し戻り値とします

時刻を format 文字列に従って文字列に変換した結果を返します。
引用:Ruby2.5.0リファレンス:Time#strftime

今回の引数の値の記号の意味はこちらになります

  • %Y…西暦を表す数
  • %m…月を表す数字(01-12)
  • %d…日(01-31)
  • %H…24時間制の時(00-23)
  • %M…分(00-59)
  • %S…秒(00-60) (60はうるう秒)

次にformatted_offset(false) を見てみます

active_support > core_ext > time > conversions.rb
class Time

  省略

  # Returns a formatted string of the offset from UTC, or an alternative
  # string if the time zone is already UTC.
  #
  #   Time.local(2000).formatted_offset        # => "-06:00"
  #   Time.local(2000).formatted_offset(false) # => "-0600"
  def formatted_offset(colon = true, alternate_utc_string = nil)
    utc? && alternate_utc_string || ActiveSupport::TimeZone.seconds_to_utc_offset(utc_offset, colon)
  end

utc? && alternate_utc_stringUTCなのか、またはalternate_urc_stringを設定しているかを判定しています
設定している場合は戻り値が設定値になります

f:id:sktktk1230:20180206173019p:plain

utc? && alternate_utc_string の判定が偽の場合を見てみます

ActiveSupport::TimeZone.seconds_to_utc_offset(utc_offset, colon) は世界標準時間からの差を出力します

UTCについてはこちらの記事がわかりやすいかと思います
www.724685.com

 

18. class Date
class Date
  def as_json(options = nil) #:nodoc:
    if ActiveSupport::JSON::Encoding.use_standard_json_time_format
      strftime("%Y-%m-%d")
    else
      strftime("%Y/%m/%d")
    end
  end
end

if ActiveSupport::JSON::Encoding.use_standard_json_time_format は 17. class Timeを参照してください
レシーバを文字列変換する処理をしています
strftimeは同様に 17. class Time を参照してください

19. class DateTime
class DateTime
  def as_json(options = nil) #:nodoc:
    if ActiveSupport::JSON::Encoding.use_standard_json_time_format
      xmlschema(ActiveSupport::JSON::Encoding.time_precision)
    else
      strftime("%Y/%m/%d %H:%M:%S %z")
    end
  end
end
  1. class Timeと処理が似ています
    use_standard_json_time_format が真の場合は、 time_precision で指定した桁数でミリ秒で文字列へ変換し戻り値とします
    偽の場合も同様に文字列に変換しています
20. class URI::Generic
class URI::Generic #:nodoc:
  def as_json(options = nil)
    to_s
  end
end
21. class Pathname
class Pathname #:nodoc:
  def as_json(options = nil)
    to_s
  end
end
22. class Process::Status
class Process::Status #:nodoc:
  def as_json(options = nil)
    { exitstatus: exitstatus, pid: pid }
  end
end

Process::Status クラスがどのようなものか調べてみると

プロセスの終了ステータスを表すクラスです。 メソッド Process.#wait2 などの返り値として使われます。
引用:Ruby2.5.0:class Process::Status

exitstatus

exited? が真の場合プロセスが返した終了ステータスの整数を、そ うでない場合は nil を返します。
引用:Ruby2.5.0:class Process::Status

pid

終了したプロセスのプロセス ID を返します。
引用:Ruby2.5.0:class Process::Status

それぞれの値をハッシュにし戻り値としています

23. class Exception
class Exception
  def as_json(options = nil)
    to_s
  end
end

読んでみて

JSON化するために様々なクラスをオープンクラスしていたので、Railsを使わずにJSONの処理をするときには気をつけないと思っていない挙動することがありそうだと思いました

Railsのソースコード読んでみる | Active Support to_json編

f:id:sktktk1230:20171212153754p:plain

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

読めるようにするまで

以前書いた記事で読めるようにするまでの設定を画像キャプチャ付きで解説しましたので、よろしければこちらをご参照下さい
shitake4.hatenablog.com

読んだ箇所

2.11 JSON support

Active Supportが提供するto_jsonメソッドの実装は、通常json gemがRubyオブジェクトに対して提供しているto_jsonよりも優れています。その理由は、HashやOrderedHash、Process::Statusなどのクラスでは、正しいJSON表現を提供するために特別な処理が必要になるためです。
引用:RAILS GUIDES:Active Support コア拡張機能:JSON

JSON を見てみます

どんな使い方だっけ?

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

Returns a JSON string representing the hash. Without any options, the returned JSON string will include all the hash keys. For example:

  { :name => "Konata Izumi", 'age' => 16, 1 => 2 }.to_json
  # => {"name": "Konata Izumi", "1": 2, "age": 16}  

引用:API dock:to_json

レシーバに対して to_jsonすると、JSON文字列を返すという処理です

thinkit.co.jp

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

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

f:id:sktktk1230:20180201183238p:plain

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

1. activesupport > lib > active_support > core_ext > object > json.rb
1. module ActiveSupport::ToJsonWithActiveSupportEncoder
省略

module ActiveSupport
  module ToJsonWithActiveSupportEncoder # :nodoc:
    def to_json(options = nil)
      if options.is_a?(::JSON::State)
        # Called from JSON.{generate,dump}, forward it to JSON gem's to_json
        super(options)
      else
        # to_json is being invoked directly, use ActiveSupport's encoder
        ActiveSupport::JSON.encode(self, options)
      end
    end
  end
end

メソッド to_json のみ実装されたモジュールです さらに読み進めてみます

省略

[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass, Enumerable].reverse_each do |klass|
  klass.prepend(ActiveSupport::ToJsonWithActiveSupportEncoder)
end

reverse_each の挙動を調べてみます

reverse_eachメソッドは、配列の要素の数だけブロックを繰り返し実行します。繰り返しごとにブロック引数には各要素が末尾から逆順に入ります。戻り値はレシーバ自身です。
animals = ["dog", "cat", "mouse"]
animals.reverse_each {|anim| puts anim }
mouse  
cat  
dog

引用:Rubyリファレンス:reverse_each

Enumerableから順番に klass へ各要素が入ります

次にブロックの中

klass.prepend(ActiveSupport::ToJsonWithActiveSupportEncoder)

を見てみます

prependがわからないので、調べてみると

指定したモジュールを self の継承チェインの先頭に「追加する」ことで self の定数、メソッド、モジュール変数を「上書き」します。
継承チェイン上で、self のモジュール/クラスよりも「手前」に 追加されるため、結果として self で定義されたメソッドは override されます。
modules で指定したモジュールは後ろから順に処理されるため、 modules の先頭が最も優先されます。
また、継承によってこの「上書き」を処理するため、prependの引数として 渡したモジュールのインスタンスメソッドでsuperを呼ぶことで self のモジュール/クラスのメソッドを呼び出すことができます。
実際の処理は modules の各要素の prepend_features を後ろから順に呼びだすだけです。 Module#prepend_features が継承チェインの改変を実行し、結果として上のような 処理が実現されます。そのため、prepend_features を override することで prepend の処理を追加/変更できます。
引用:Ruby2.5.0:prepend

配列のクラス [Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass, Enumerable] にモジュールを追加して to_json をオーバーライドしてます
再度、ActiveSupport::ToJsonWithActiveSupportEncoderを見てみると

def to_json(options = nil)
  if options.is_a?(::JSON::State)
    # Called from JSON.{generate,dump}, forward it to JSON gem's to_json
    super(options)
  else
    # to_json is being invoked directly, use ActiveSupport's encoder
    ActiveSupport::JSON.encode(self, options)
  end
end

optionに ::JSON::State を指定した場合には、super(options) により継承元の to_json を呼んでいます
それ以外は ActiveSupport::JSON.encode(self, options) となります

encodeの処理を見てみます

2. activesupport > lib > active_support > json > encoding.rb
module JSON
  # Dumps objects in JSON (JavaScript Object Notation).
  # See http://www.json.org for more info.
  #
  #   ActiveSupport::JSON.encode({ team: 'rails', players: '36' })
  #   # => "{\"team\":\"rails\",\"players\":\"36\"}"
  def self.encode(value, options = nil)
    Encoding.json_encoder.new(options).encode(value)
  end

引数valueに入るのは to_json した際のレシーバです

次に Encoding.json_encoderまでみてみます

module ActiveSupport

省略

  module JSON

省略

    module Encoding #:nodoc:

省略

    self.json_encoder = JSONGemEncoder
    self.time_precision = 3
    end

module Encoding内でjson_encoderにJSONGemEncoderクラスをセットしています

JSONGemEncoderクラスのinitializerでは引数のオプションをインスタンス変数に入れています

module Encoding #:nodoc:
  class JSONGemEncoder #:nodoc:
    attr_reader :options

    def initialize(options = nil)
      @options = options || {}
    end

ここまでみると Encoding.json_encoder.new(options) の戻り値は JSONGemEncoderクラスのインスタンスです

省略

class JSONGemEncoder #:nodoc:

省略

# Encode the given object into a JSON string
def encode(value)
  stringify jsonify value.as_json(options.dup)
end

上記は括弧が省略されていますが、 stringify(jsonify(value.as_json(options.dup))) と同義です
value.as_jsonvalueto_json した際のレシーバですので、ハッシュなどになります

jsonifyをみてみます

# Convert an object into a "JSON-ready" representation composed of
# primitives like Hash, Array, String, Numeric,
# and +true+/+false+/+nil+.
# Recursively calls #as_json to the object to recursively build a
# fully JSON-ready object.
#
# This allows developers to implement #as_json without having to
# worry about what base types of objects they are allowed to return
# or having to remember to call #as_json recursively.
#
# Note: the +options+ hash passed to +object.to_json+ is only passed
# to +object.as_json+, not any of this method's recursive +#as_json+
# calls.
def jsonify(value)
  case value
  when String
    EscapedString.new(value)
  when Numeric, NilClass, TrueClass, FalseClass
    value.as_json
  when Hash
    Hash[value.map { |k, v| [jsonify(k), jsonify(v)] }]
  when Array
    value.map { |v| jsonify(v) }
  else
    jsonify value.as_json
  end
end

Stringクラスの場合には

# This class wraps all the strings we see and does the extra escaping
class EscapedString < String #:nodoc:
  def to_json(*)
    if Encoding.escape_html_entities_in_json
      super.gsub ESCAPE_REGEX_WITH_HTML_ENTITIES, ESCAPED_CHARS
    else
      super.gsub ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, ESCAPED_CHARS
    end
  end

  def to_s
    self
  end
end

上記EscapedStringクラスに値を入れ直して、newしています

Numeric, NilClass, TrueClass, FalseClassの場合には、as_jsonするだけになります

Hashクラスの場合もみてみます

Hash[value.map { |k, v| [jsonify(k), jsonify(v)] }]

まずvalue.map { |k, v| [jsonify(k), jsonify(v)] } ではvalue(ハッシュ)のkey,valueをそれぞれjsonifyし、2次元配列を作っています
※ 2次元配列はこのようなデータです [['hoge', 'huga'],['piyo', 'poyo']]

メソッドの内部で再度同じメソッドを呼ぶことが不思議に思えるかもしれませんが、再帰といいます

再帰とはどのようなものかはこちらの記事がわかりやすいかと思います
再帰呼び出し

mapはブロックの戻り値を配列化します

ブロックの内部ではハッシュのキー、バリューを jsonifyし配列を作成しています

Hash[] の処理を調べてみます

新しいハッシュを生成します。 引数は必ず偶数個指定しなければなりません。奇数番目がキー、偶数番目が値になります。

このメソッドでは生成するハッシュにデフォルト値を指定することはできません。 Hash.newを使うか、Hash#default=で後から指定してください。

[PARAM] key_and_value:

生成するハッシュのキーと値の組です。必ず偶数個(0を含む)指定しなければいけません。

[EXCEPTION] ArgumentError:

奇数個の引数を与えたときに発生します。

以下は配列からハッシュを生成する方法の例です。


省略


4) キーや値が配列の場合

alist = [[1,["a"]], [2,["b"]], [3,["c"]], [[4,5], ["a", "b"]]]
hash = Hash[alist] # => {1=>["a"], 2=>["b"], 3=>["c"], [4, 5]=>["a", "b"]}

引用:Ruby2.5.0:Hash

hashの各キー、バリューをjsonifyし、再度ハッシュを作り直すという処理になります

Arrayクラスの場合を見てみます
value.map { |v| jsonify(v) } 配列の各要素に対して jsonify しています

上記以外の場合は

jsonify value.as_json

引数を as_json しているので、上記クラスのどれかになるまで処理が繰り返されます

ここまでが to_json の処理になります

json.rb ではさきほど見てきた処理以外にもオープンクラスして as_json メソッドを定義している箇所があります
こちらについては別記事にまとめようと思います

読んでみて

json化する処理はよく使ったりするので、調べてみて内部で何をしているのか把握することは大切だと感じました

Railsのソースコード読んでみる | Active Support with_options編

f:id:sktktk1230:20171212153754p:plain

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

読めるようにするまで

以前書いた記事で読めるようにするまでの設定を画像キャプチャ付きで解説しましたので、よろしければこちらをご参照下さい
shitake4.hatenablog.com

読んだ箇所

with_options を今日は読んでみようと思います

どんな使い方だっけ?

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

with_optionsメソッドは、連続した複数のメソッド呼び出しに対して共通して与えられるオプションを解釈するための手段を提供します。
デフォルトのオプションがハッシュで与えられると、with_optionsはブロックに対するプロキシオブジェクトを生成します。
そのブロック内では、プロキシに対して呼び出されたメソッドにオプションを追加したうえで、そのメソッドをレシーバに転送します。
たとえば、以下のように同じオプションを繰り返さないで済むようになります。

class Account < ActiveRecord::Base  
  has_many :customers, dependent: :destroy  
  has_many :products,  dependent: :destroy
  has_many :invoices,  dependent: :destroy
  has_many :expenses,  dependent: :destroy
 end  

上は以下のようにできます。

class Account < ActiveRecord::Base
  with_options dependent: :destroy do |assoc|
    assoc.has_many :customers
    assoc.has_many :products
    assoc.has_many :invoices
    assoc.has_many :expenses
  end
end

引用:RAILS GUIDES:Active Support コア拡張機能#with_options

読んでみましたが、イマイチ使い方がわからなかったので、さらに調べてみます

with_optionsの引数に渡したHashのオプションの値がブロック内に適用されます。上の例ではhash_manyのdependentオプションを共通化しましたが、他のパターンでも適用できます。例えば、モデルの例では他にもvalidatesメソッドのオプションを共通化など使えます。
引用:Railsのwith_options

上記例であれば、ハッシュである dependent: :destroy を共通化して各 has_many :xxx の引数に渡すという感じです

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

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

f:id:sktktk1230:20180126135718p:plain

2. 該当箇所をみてみます

1. activesupport > lib > active_support > core_ext > object > with_options.rb
# frozen_string_literal: true

require "active_support/option_merger"

class Object

省略

def with_options(options, &block)
  option_merger = ActiveSupport::OptionMerger.new(self, options)
  block.arity.zero? ? option_merger.instance_eval(&block) : block.call(option_merger)
end

まず引数を見てみます
さきほどの例だと optionsdependent: :destroy となります
&block

do |assoc|
  assoc.has_many :customers
  assoc.has_many :products
  assoc.has_many :invoices
  assoc.has_many :expenses
end

までとなります

それではwith_optionsのメソッドのロジックをみていきます
一行目 option_merger = ActiveSupport::OptionMerger.new(self, options)ActiveSupport::OptionMerger が何をしているのかわからないので、 こちらのコードを読んでみます

activesupport > lib > active_support > option_merger.rb
# frozen_string_literal: true

require "active_support/core_ext/hash/deep_merge"

module ActiveSupport
  class OptionMerger #:nodoc:
    instance_methods.each do |method|
      undef_method(method) if method !~ /^(__|instance_eval|class|object_id)/
    end

    def initialize(context, options)
      @context, @options = context, options
    end

    private
      def method_missing(method, *arguments, &block)
        if arguments.first.is_a?(Proc)
          proc = arguments.pop
          arguments << lambda { |*args| @options.deep_merge(proc.call(*args)) }
        else
          arguments << (arguments.last.respond_to?(:to_hash) ? @options.deep_merge(arguments.pop) : @options.dup)
        end

        @context.__send__(method, *arguments, &block)
      end
  end
end

newした際の引数self, optionsインスタンス変数 context, optionsにそれぞれ格納されます

次に block.arity.zero? ? option_merger.instance_eval(&block) : block.call(option_merger) です

条件式 ? 真(true)の場合に実行される : 偽(false)の場合に実行される という書き方は三項演算子というものです

arity を調べると

メソッドが受け付ける引数の数を返します。
ただし、メソッドが可変長引数を受け付ける場合、負の整数
引用:Ruby2.5.0リファレンスマニュアル:arity

zero? を調べると

自身がゼロの時、trueを返します。そうでない場合は false を返します。 引用:Ruby2.5.0リファレンスマニュアル:arity

つまり、条件式 block.arity.zero? はブロックが受け付ける引数の数がゼロかを判定しています

条件式が真の場合に実行される option_merger.instance_eval(&block) を見てみます
instance_evalを調べてみると

instance_evalメソッドは、渡されたブロックをレシーバのインスタンスの元で実行します。ブロックの戻り値がメソッドの戻り値になります。
ブロック内では、インスタンスメソッド内でコードを実行するときと同じことができます。ブロック内でのselfはレシーバのオブジェクトを指します。なお、ブロックの外側のローカル変数はブロック内でも使えます。
引用:Rubyリファレンス:instance_eval

つまりoption_merger.instance_eval(&block) とはインスタンスoption_mergerでブロックの内容が実行されるということです
このようなのブロックの場合

do
  has_many :customers
  has_many :products
  has_many :invoices
  has_many :expenses
end

で定義されていた has_many メソッドは option_mergerで定義されていないので、 最終的に method_missing となります

※ method_missingについて参考: qiita.com

呼び出されるmethod_missingはoption_mergerのプライベートメソッドでオーバーライドしている method_missingとなります

private
  def method_missing(method, *arguments, &block)
    if arguments.first.is_a?(Proc)
      proc = arguments.pop
      arguments << lambda { |*args| @options.deep_merge(proc.call(*args)) }
    else
      arguments << (arguments.last.respond_to?(:to_hash) ? @options.deep_merge(arguments.pop) : @options.dup)
    end

    @context.__send__(method, *arguments, &block)
  end

method_missing の引数に入る値はさきほどのブロックの例だと method = hash_many , *arguments = [:customers] です

if から else までみてみます

if arguments.first.is_a?(Proc)
  proc = arguments.pop
  arguments << lambda { |*args| @options.deep_merge(proc.call(*args)) }
else

arguments配列の先頭がProcだった場合という意味になります
なぜこの条件式があるのかわからなかったので、調べてみます
RubyMineでdef method_missing end を選択し、選択箇所のコミットを見てみます

f:id:sktktk1230:20180126135748p:plain

option_mergerでlambdaもマージできるようにしたということだけがわかりましたが、どんなコードになるのか想像できなかったので、テストケースをみてみます
同じコミットにテストケースが1つ追加されていました

def test_nested_method_with_options_using_lambda
  local_lambda = lambda { { lambda: true } }
  with_options(@options) do |o|
    assert_equal @options.merge(local_lambda.call),
      o.method_with_options(local_lambda).call
  end
end

ハッシュを返すlambdaを定義しています
@optionsmethod_with_options は同じクラス内に定義されていたので、見てみると

class OptionMergerTest < ActiveSupport::TestCase
  def setup
    @options = { hello: "world" }
  end

省略

  private
    def method_with_options(options = {})
      options
    end
end

ブロック内で定義されているメソッドのoptionに当たる部分がlambdaだった場合に、正しく動くかをテストしてます
テストケースを見てみると、このコミットでの変更はメソッドのoptionsがハッシュを返すlambdaだった場合も正しく動くようにするという修正であることがわかりました

次に proc = arguments.pop を見ます

popメソッドは、配列の末尾の要素を削除し、その要素を返します。レシーバ自身を変更するメソッドです。配列が空のときはnilを返します。
引用:Rubyリファレンス:pop

else から end までみてみます

else
  arguments << (arguments.last.respond_to?(:to_hash) ? @options.deep_merge(arguments.pop) : @options.dup)
end

三項演算子の条件式部分の (arguments.last.respond_to?(:to_hash) をみます

argumentsに入るのはブロックで定義したメソッドの引数部分となります
optionがあるメソッドの定義は基本的に

def hoge(value, option)
end

のように引数の最後に記述することが多いので、引数の最後の値がハッシュを返すかを確認しています
to_hash はHashやArrayなどで定義されています

三項演算子の判定結果が真の場合 @options.deep_merge(arguments.pop) を見てみます

@options は with_optionsメソッドで第一引数に入れたハッシュでした
そのハッシュに対してdeep_mergeしているということです

deep_mergeについて調べてみると

先の例で説明したとおり、キーがレシーバと引数で重複している場合、引数の側の値が優先されます。
Active SupportではHash#deep_mergeが定義されています。ディープマージでは、レシーバと引数の両方に同じキーが出現し、さらにどちらも値がハッシュである場合に、その下位のハッシュを マージ したものが、最終的なハッシュで値として使用されます。
引用:RAILS GUIDES:Active Support コア拡張機能#deep_merge

arguments.pop は、さきほどの例def hoge(value, option) でいうと arguments = [value, option] となっているのを .pop することで option を取り出し arguments = [value] へと破壊的変更をすることです

判定結果が偽の場合 @options.dup も見ます

cloneメソッドとdupメソッドは、レシーバのオブジェクトのコピーを作成して返します。オブジェクトのコピーとは、同じ内容を持つ別のオブジェクトです。具体的には、元のオブジェクトと同じクラスの新しいオブジェクトで、元のオブジェクトのインスタンス変数を新しいオブジェクトにコピーしたものです。
引用:Rubyリファレンス:clone, dup

with_optionsメソッドで第一引数に入れたハッシュをコピーして返却するだけです

arguments << はそれぞれの判定結果の戻り値ハッシュをargumentsに追加しています

最後に @context.__send__(method, *arguments, &block) をみます

@context はwith_optionsのレシーバです
__send__ を調べてみます

sendメソッドは、sendの別名です。レシーバの持っているメソッドを呼び出します。
引用:Rubyリファレンス:send

with_optionsのレシーバに対してブロックで定義したメソッドを実行しています

読んでみて

method_missingを利用したプログラミングを初めて見ることができたので、非常に勉強になりました

はてなブログで引用文内にコードを挿入する方法

f:id:sktktk1230:20180126163944p:plain

マークダウン記法で記述している際に、引用した文章内にコードブロックを挿入したかったのですが、うまくできずハマったので、 解決方法を書いてみます

やりたいこと

こちらのコードはRubyで書かれています
Class Hoge
  def huga
    puts 'huga!'
  end
end

解決方法

HTMLタグ <blockquote> 内にコードブロックを書く

書き方

f:id:sktktk1230:20180126161536p:plain

Railsのソースコード読んでみる | Active Support to_query編

f:id:sktktk1230:20171212153754p:plain

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

読めるようにするまで

以前書いた記事で読めるようにするまでの設定を画像キャプチャ付きで解説しましたので、よろしければこちらをご参照ください
shitake4.hatenablog.com

読んだ箇所

to_query を今日は読んでみようと思います

どんな使い方だっけ?

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

このメソッドは、エスケープされていないkeyを受け取ると、そのキーをto_paramが返す値に対応させるクエリ文字列の一部を生成します
引用:Active Support コア拡張機能:present

使い方はこんな感じのようです

current_user.to_query('user') # => "user=357-john-smith"

引数=レシーバを変換した値 という文字列を生成します

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

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

f:id:sktktk1230:20180118143800p:plain

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

activesupport > lib > active_support > core_ext > object > to_query.rb
1. class Object
class Object

省略

  # Converts an object into a string suitable for use as a URL query string,
  # using the given <tt>key</tt> as the param name.
  def to_query(key)
    "#{CGI.escape(key.to_param)}=#{CGI.escape(to_param.to_s)}"
  end
end

まず CGI.escape() を見てみます

与えられた文字列を URL エンコードした文字列を新しく作成し返します。
[PARAM] string:
URL エンコードしたい文字列を指定します。

引用:Rubyリファレンスマニュアル:CGI.escape

文字列の引数を1つ取り、それに対してURLエンコードする処理です

CGI.escape() の引数部分 key.to_param で何をやっているのかは以前書いたこちらを参照ください

shitake4.hatenablog.com

続いて、 CGI.escape(to_param.to_s)を見ます

CGI.escape はさきほどと同様です
to_param.to_s はレシーバを to_param し、その戻り値を to_sしています

to_s はレシーバを文字列に変換するメソッドです
次のクラスに実装されています

ここまでをまとめると
key=valueという文字列を生成するメソッドになります

key部分を生成する際には to_query の引数に対して to_paramし、それをHTMLエスケープして作ります
value部分を生成する際には レシーバを to_paramし、それをHTMLエスケープするということです

2. class Array
class Array

省略

  # Converts an array into a string suitable for use as a URL query string,
  # using the given +key+ as the param name.
  #
  #   ['Rails', 'coding'].to_query('hobbies') # => "hobbies%5B%5D=Rails&hobbies%5B%5D=coding"
  def to_query(key)
    prefix = "#{key}[]"

    if empty?
      nil.to_query(prefix)
    else
      collect { |value| value.to_query(prefix) }.join "&"
    end
  end
end

変数prefixの値の例を見てみます
仮にkeyがrubyMineだとすると

prefix = "rubyMine[]"

という文字列になります

次にif empty? を見てみます
empty?はRubyの標準メソッドです
使い方を見てみると

empty?メソッドは、配列が空であればtrue、1つ以上の要素があればfalseを返します。
引用:Rubyリファレンス:empty?

if empty? はレシーバである配列の中身が空であるかを判定しています

次の処理nil.to_query(prefix)を見てみます
どんな挙動かというと
f:id:sktktk1230:20180118144405p:plain

最後に collect { |value| value.to_query(prefix) }.join "&" を見ます
collect { |value| value.to_query(prefix) } では

レシーバに対して collect を実行しています
collectとはどういうものかというと

collectメソッドは、要素の数だけ繰り返しブロックを実行し、ブロックの戻り値を集めた配列を作成して返します。ブロック引数itemには各要素が入ります。
mapメソッドはcollectメソッドの別名です。
次の例では、16進数を表す文字列を数値に変換した配列を作成しています。
引用: Rubyリファレンス:collect

numbers = ["68", "65", "6C", "6C", "6F"]
p numbers.collect {|item| item.to_i(16) }

=> [104, 101, 108, 108, 111]

引用: Rubyリファレンス:collect

レシーバの中身1つずつに対して、value.to_query(prefix) を実行しています
そして戻り値を配列にしています
実行例としてはこちらになります
f:id:sktktk1230:20180118144355p:plain

.join "&" では配列中身を&で連結し文字列生成しています

joinの挙動はこちらです

joinメソッドは、配列の各要素を文字列に変換し、引数sepを区切り文字として結合した文字列を返します。
引用:Rubyリファレンス:join

読んでみて

HTTPGetメソッドでパラメータを生成する際の使用するメソッドはWebサービスを作る上で欠かせないものだったりするので、 他のフレームワークや言語で作る際にはここで読んだ内容が活かせるなと思いました

Railsのソースコード読んでみる | Active Support to_param編

f:id:sktktk1230:20171212153754p:plain

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

読めるようにするまで

以前書いた記事に読めるようにするまでの設定を画像キャプチャ付きで解説しましたので、よろしければこちらをご参照ください
shitake4.hatenablog.com

読んだ箇所

to_param を今日は読んでみようと思います

どんな使い方だっけ?

読んでみる前にまずは使い方を調べてみます
RailsGuidのActive Support コア拡張機能を見てみると

Railsのあらゆるオブジェクトはto_paramメソッドに応答します。
これは、オブジェクトを値として表現するものを返すということです。返された値はクエリ文字列やURLの一部で使用できます。
引用:Active Support コア拡張機能:to_param

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

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

f:id:sktktk1230:20180115104828p:plain

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

1. activesupport > lib > active_support > core_ext > object > to_query.rb
1. class Object
# frozen_string_literal: true

require "cgi"

class Object
  # Alias of <tt>to_s</tt>.
  def to_param
    to_s
  end

単純にto_s しているだけです
オブジェクトに対して行いたい場合は、オーバーライドして使うということを想定しているのでしょう

2. class NilClass
class NilClass
  # Returns +self+.
  def to_param
    self
  end
end

レシーバ自身を返しています NilClassの戻り値はnilですので、このようなコードでもいいのではないか?と思いました

def to_param
  nil
end

もしかしたらコミットログに戻り値をselfにした理由や経緯があるかもしれないと思ったので、調べてみます
RubyMineの機能で選択範囲のコミットログを見る機能がある為、それを使います f:id:sktktk1230:20180115104852p:plain

残念ながら1コミットしかなかったため、わかりませんでした
f:id:sktktk1230:20180115104905p:plain

3. class TrueClass
class TrueClass
  # Returns +self+.
  def to_param
    self
  end
end
4. class FalseClass
class FalseClass
  # Returns +self+.
  def to_param
    self
  end
end
5. class Array
class Array
  # Calls <tt>to_param</tt> on all its elements and joins the result with
  # slashes. This is used by <tt>url_for</tt> in Action Pack.
  def to_param
    collect(&:to_param).join "/"
  end

レシーバに対して collect を実行しています
collectとはどういうものかというと

collectメソッドは、要素の数だけ繰り返しブロックを実行し、ブロックの戻り値を集めた配列を作成して返します。ブロック引数itemには各要素が入ります。
mapメソッドはcollectメソッドの別名です。
次の例では、16進数を表す文字列を数値に変換した配列を作成しています。
引用: Rubyリファレンス:collect

numbers = ["68", "65", "6C", "6C", "6F"]
p numbers.collect {|item| item.to_i(16) }

=> [104, 101, 108, 108, 111]

引用: Rubyリファレンス:collect

mapも同じ処理を行うメソッドです
(&:to_param) の&:は配列の各要素に対して to_param を実行しています
たとえば、

[1, 2, 3].map(&:to_s)
=> ["1", "2", "3"]

という感じです ※詳しい解説は@kasei-san氏のこちらの記事がわかりやすいかと思います
qiita.com

各要素に to_param した配列に対して .join しています
joinについて調べてみると

joinメソッドは、配列の各要素を文字列に変換し、引数sepを区切り文字として結合した文字列を返します。
引数のデフォルト値は組み込み変数$,の値です。$,の初期値はnilなので、引数を省略すると区切り文字なしで要素を結合した文字列になります。
引用: Rubyリファレンス:join

配列の中身を連結し文字列に変換する処理です
たとえば、

array = ["Ruby", "Mine"]
puts array.join(", ")

の場合は Ruby, Mine と出力されます

区切り文字がなが場合は

array = ["Ruby", "Mine"]
puts array.join

RubyMine というようにそのまま連結し出力します

ということで .join "/" ではto_param された各要素を"/"で連結し、文字列として出力するという処理になります

2. activesupport > lib > active_support > core_ext > string > output_safety.rb
module ActiveSupport #:nodoc:
  class SafeBuffer < String

中略
    def to_param
      to_str
    end

Stringを拡張したSafeBufferクラスのインスタンスに対して to_param するのは to_str と同様になります
SafeBufferクラスがどのような用途で使われるものなのか分からない為、さきほどと同様にコミットログから理由、経緯を調べてみます

選択範囲をSafeBufferクラス内にしコミットログを調べます

class SafeBuffer < String

省略

end

f:id:sktktk1230:20180115104938p:plain

一番古いコミットログを見付け足ので、こちらのリビジョンナンバーをコピーしRailsリポジトリで調べます

f:id:sktktk1230:20180115104951p:plain

GitHubの検索窓にさきほどコピーしたリビジョンナンバーをペーストし検索します
f:id:sktktk1230:20180115105007p:plain

該当のコミットを見てみると
f:id:sktktk1230:20180115105020p:plain

パフォーマンスの改善の為 html_safe を呼び出す場合にはSafeBufferを使うということでした
コメントも読んでみると
f:id:sktktk1230:20180115105032p:plain

Stringに追加していると +<< を実行する時に遅くなっているということでした

読んでみて

コミットログを追ってみるとなぜ実装されているのかが分かるので、コードを読み込むだけでなく、歴史まで追ってみるとより理解が深まるなと思いました

Railsのソースコード読んでみる | Active Support acts_like?編

f:id:sktktk1230:20171212153754p:plain

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

読めるようにするまで

以前書いた記事に読めるようにするまでの設定を画像キャプチャ付きで解説しましたので、よろしければこちらをご参照ください
shitake4.hatenablog.com

読んだ箇所

acts_like? を今日は読んでみようと思います

どんな使い方だっけ?

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

2.7 acts_like?(duck)
acts_like?メソッドは、一部のクラスがその他のクラスと同様に振る舞うかどうかのチェックを、ある慣例に則って実行します。Stringクラスと同じインターフェイスを提供するクラスがあり、その中で以下のメソッドを定義しておくとします。
def acts_like_string?
end
このメソッドは単なる目印であり、メソッドの本体と戻り値の間には関連はありません。これにより、クライアントコードで以下のようなダックタイピングチェックを行なうことができます。
some_klass.acts_like?(:string)
RailsにはDateクラスやTimeクラスと同様に振る舞うクラスがいくつかあり、この手法を使用できます。
引用:ActiveSupport コア機能:acts_like?

レシーバのクラスが引数に入れたクラスと同じ振る舞いをするか確認するメソッドです
安全にダックタイピングする為、レシーバを確認したい場合などに利用します

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

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

f:id:sktktk1230:20180111152611p:plain

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

activesupport > lib > active_support > core_ext > object > acts_like.rb
# frozen_string_literal: true

class Object
  # A duck-type assistant method. For example, Active Support extends Date
  # to define an <tt>acts_like_date?</tt> method, and extends Time to define
  # <tt>acts_like_time?</tt>. As a result, we can do <tt>x.acts_like?(:time)</tt> and
  # <tt>x.acts_like?(:date)</tt> to do duck-type-safe comparisons, since classes that
  # we want to act like Time simply need to define an <tt>acts_like_time?</tt> method.
  def acts_like?(duck)
    case duck
    when :time
      respond_to? :acts_like_time?
    when :date
      respond_to? :acts_like_date?
    when :string
      respond_to? :acts_like_string?
    else
      respond_to? :"acts_like_#{duck}?"
    end
  end
end

レシーバに acts_like_引数 メソッドが実装されているか確認しています
acts_like_引数 メソッドの内、time, date, stringはactivesupport内ですでに実装されているということがここらか分かりました
それ以外にも拡張することもできるようです

それでは acts_like_引数 はどう実装すればいいのか見てみます

3. まずはdef acts_like_time? を探してみます

f:id:sktktk1230:20180111152627p:plain

4. 該当箇所が4箇所あったので、それぞれ見てみます

そのままtrueで返しています

1. activesupport > lib > active_support > core_ext > date_time > acts_like.rb
# frozen_string_literal: true

require "date"
require "active_support/core_ext/object/acts_like"

class DateTime
  # Duck-types as a Date-like class. See Object#acts_like?.
  def acts_like_date?
    true
  end

  # Duck-types as a Time-like class. See Object#acts_like?.
  def acts_like_time?
    true
  end
end
2. activesupport > lib > active_support > time_with_zone.rb
# So that +self+ <tt>acts_like?(:time)</tt>.
def acts_like_time?
  true
end
3. activesupport > lib > active_support > core_ext > time > acts_like.rb
# frozen_string_literal: true

require "active_support/core_ext/object/acts_like"

class Time
  # Duck-types as a Time-like class. See Object#acts_like?.
  def acts_like_time?
    true
  end
end
4. activesupport > test > core_ext > object > acts_like_test.rb

テスト用に定義されたものの為、省略します

4. 次にdef acts_like_date? を探してみます

f:id:sktktk1230:20180111152644p:plain

5. 該当箇所が2箇所あったので、それぞれ見てみます

さきほどと重複になりますが、そのままtrueを返しています

1. activesupport > lib > active_support > core_ext > date_time > acts_like.rb
# frozen_string_literal: true

require "date"
require "active_support/core_ext/object/acts_like"

class DateTime
  # Duck-types as a Date-like class. See Object#acts_like?.
  def acts_like_date?
    true
  end

  # Duck-types as a Time-like class. See Object#acts_like?.
  def acts_like_time?
    true
  end
end
2. activesupport > lib > active_support > core_ext > date > acts_like.rb
# frozen_string_literal: true

require "active_support/core_ext/object/acts_like"

class Date
  # Duck-types as a Date-like class. See Object#acts_like?.
  def acts_like_date?
    true
  end
end

5. 最後にdef acts_like_string? を探してみます

f:id:sktktk1230:20180111152706p:plain

5. 該当箇所が2箇所あったので、それぞれ見てみます

さきほどと重複になりますが、そのままtrueを返しています

1. activesupport > lib > active_support > core_ext > string > behavior.rb
# frozen_string_literal: true

class String
  # Enables more predictable duck-typing on String-like classes. See <tt>Object#acts_like?</tt>.
  def acts_like_string?
    true
  end
end
2. guides > source > active_support_core_extensions.md

ソースコードではなくactivesupportのcore_extensionのドキュメントだったのでここでは省略します

6. 一応さきほど調べた以外の def acts_like_xxx が存在しないか確認してみます

他の実装はありませんでした
f:id:sktktk1230:20180111152718p:plain

読んでみて

安全なダックタイピングをするために acts_like_xxx が実装されているのは知りませんでした
ダックタイピングをする場合には、このメソッドを活用していければと思います

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

f:id:sktktk1230:20171212153754p:plain

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

読めるようにするまで

以前書いた記事に読めるようにするまでの設定を画像キャプチャ付きで解説しましたので、よろしければこちらをご参照下さい
shitake4.hatenablog.com

読んだ箇所

仕事でもたまに使われてたりする class_eval を今日は読んでみようと思います

どんな使い方だっけ?

読んでみる前にまずは使い方を調べてみます
Ruby on RailsAPIドキュメントを見てみると

class_eval on an object acts like singleton_class.class_eval.
引用:Ruby on Rails API:class_eval

調べてみてもどんな動きをするのか分かりませんでした singleton_class.class_eval のように振る舞うそうなので、調べてみます

まずはsingleton_classです

singleton_classメソッドは、オブジェクトの特異クラスを返します。
小さい整数(Fixnum)およびシンボルに対してsingleton_classを呼び出すと、例外TypeErrorが発生します。true、false、nilに対して呼び出すと、特異クラスではなくTrueClass、FalseClass、NilClassを返します。
次の例では、オブジェクトcatの特異クラスをsingletonに取り出し、define_methodで特異メソッドを定義しています。
引用:Rubyリファレンス:singleton_class

次にclass_evalです

class_evalメソッドは、ブロックをクラス定義やモジュール定義の中のコードであるように実行します。ブロックの戻り値がメソッドの戻り値になります。
引用:Rubyリファレンス:class_eval

使い方はこんなかんじです

class User  
  attr_accessor :name  
  def initialize(name)  
    @name = name  
  end  
  [:downcase, :upcase].each do |method|  
    class_eval <<-EOS  
      def #{method}  
        @name.#{method}  
      end  
    EOS  
  end  
end  
 
user = User.new("taro")  
puts user.upcase  

### 引用:[Rubyリファレンス:class_eval](https://ref.xaio.jp/ruby/classes/module/class_eval)

eachでdowncase,upcaseを回し、downcase,upcaseを定義しています

[:downcase, :upcase].each do |method|
  class_eval <<-EOS
    def #{method}
      @name.#{method}
    end
  EOS
end

#{}で変数の値を埋め込んでいるので、downcaseの場合はこんな感じで定義されているということです

class_eval <<-EOS
  def downcase
    @name.downcase
  end
EOS

つまり、特異クラスに対して、与えられたブロックをクラス定義の中のコードであるように実行することが出来るということです

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

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

f:id:sktktk1230:20180110160708p:plain

2. 該当箇所が1箇所だったので、それを見てみます

1. activesupport > lib > active_support > core_ext > kernel > singleton_class.rb
# frozen_string_literal: true

module Kernel
  # class_eval on an object acts like singleton_class.class_eval.
  def class_eval(*args, &block)
    singleton_class.class_eval(*args, &block)
  end
end

これを見てみるとsingleton_class.class_eval(*args, &block) を呼び出しているだけなので、このメソッドいるのかな?と思えてしまいます
なので、コミットログで現在に至るまでの経緯を見てみます

こちらがコミットログです
f:id:sktktk1230:20180110160723p:plain

ruby1.9.2でsingleton_classが実装されたようです
その為、それ以前のバージョンにも対応出来るようにsingleton_class メソッドが記述されていたようです
f:id:sktktk1230:20180110160736p:plain

一応singleton_classの実装がRuby1.9.2で行われているかリポジトリのチェンジログ等で確認してみます
RubyのGithubのdoc配下にNEWS-1.9.2があったので見てみると
f:id:sktktk1230:20180110160752p:plain

読んでみて

class_evalは使い方だけ覚えて使っていたけど、しっかり実装を追ってみたからこそわかることがあったので、引き続き続けていきます

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

f:id:sktktk1230:20171212153754p:plain

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

読めるようにするまで

以前書いた記事に読めるようにするまでの設定を画像キャプチャ付きで解説しましたので、よろしければこちらをご参照下さい
shitake4.hatenablog.com

読んだ箇所

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

どんな使い方だっけ?

読んでみる前にまずは使い方を調べてみます
RailsAPIリファレンスを見てみると

try(*a, &b)

Invokes the public method whose name goes as first argument just like public_send does, except that if the receiver does not respond to it the call returns nil rather than raising an exception.
This method is defined to be able to write

@person.try(:name)

instead of

@person.name if @person

引用:Ruby on Rails API: try

レシーバ(@person)がnilならnilを返し、別のオブジェクトならnameメソッドを実行するというかんじです

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

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

f:id:sktktk1230:20171225145117p:plain

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

1. activesupport > lib > active_support > core_ext > object > try.rb
ActiveSupport::Tryable.try
# frozen_string_literal: true

require "delegate"

module ActiveSupport
  module Tryable #:nodoc:
    def try(*a, &b)
      try!(*a, &b) if a.empty? || respond_to?(a.first)
    end

まず def try(*a, &b)を見てみます
引数の *aですが、rubyは仮引数に*をつけることで引数をすべて配列として受け取ることができます(可変長引数といいます)
&b はブロックを引数として受け取ってます

ブロックとは

eachメソッドなど、Rubyでは「ブロック」という仕組みを活用して、メソッドに「処理そのものを引数として渡す」というパターンが頻出します。
引用:Rubyの面白さを理解するためのメソッド、ブロック、Proc、lambda、クロージャの基本

次に try!(*a, &b) if a.empty? を見てみると 仮引数のaが空の場合は try!(*a, &b) を実行するようです

try!を見てみます 同じmodule内にtry!メソッドがありました

def try!(*a, &b)
  if a.empty? && block_given?
    if b.arity == 0
      instance_eval(&b)
    else
      yield self
    end
  else
    public_send(*a, &b)
  end
end

if a.empty? && block_given? は配列aに対して empty? をしています
Array.empty?を調べてみると

empty?メソッドは、配列が空であればtrue、1つ以上の要素があればfalseを返します。

arr = [1, 2, 3] puts arr.empty? arr = [] puts arr.empty?

引用:Rubyリファレンス:empty?

なので、配列の中身がnilでもfalseになります
以下が配列の中身がnilの場合の挙動です
f:id:sktktk1230:20171225145142p:plain

block_given? はメソッドにブロックが与えられているか確認するメソッドです

メソッドにブロックが与えられていれば真を返します。
このメソッドはカレントコンテキストにブロックが与えられているかを調べるので、 メソッド内部以外で使っても単に false を返します。
iterator? は (ブロックが必ずイテレートするとはいえないので)推奨されていないの で block_given? を使ってください。
Ruby2.4.0リファレンスマニュアル

つまり、仮引数aの要素が空かつブロックが渡されているときがこちらの判定式の内容です

※ ブロックについては@kidach1氏のこちらの記事が分かりやすかったです
qiita.com

次にif b.arity == 0を見てみます
arity を調べてみると

メソッドが受け付ける引数の数を返します。
ただし、メソッドが可変長引数を受け付ける場合、負の整数
-(必要とされる引数の数 + 1) を返します。C 言語レベルで実装されたメソッドが可変長引数を 受け付ける場合、-1 を返します。
引用:Ruby2.4.0リファレンスマニュアル

ブロックの引数が無い場合に真となります
その場合には instance_eval にブロックを渡すということになります

instance_evalメソッドは、渡されたブロックをレシーバのインスタンスの元で実行します。ブロックの戻り値がメソッドの戻り値になります。
ブロック内では、インスタンスメソッド内でコードを実行するときと同じことができます。ブロック内でのselfはレシーバのオブジェクトを指します。なお、ブロックの外側のローカル変数はブロック内でも使えます。
Ruby 1.9 Ruby 1.9では、instance_evalメソッドはBasicObjectに移されました(この変更はRuby 1.8用に書いたプログラムに特に影響はありません)。
次の例では、instance_evalメソッドに渡したブロック内でインスタンス変数やprivateメソッドを利用しています。

class Cat  
   def initialize(name)  
     @name = name  
   end  
   private  
   def hello  
     "meow..."  
   end  
end  
cat = Cat.new("Piko")  
puts cat.instance_eval { @name + ": " + hello }  

引用:Rubyリファレンス:instance_eval

yield self はブロックが引数を受け取る場合を想定しています
ブロックが引数を受け取るのはどんな記述かというとeachメソッドであればこのような書き方です

each do |i|
  puts i
end

次に if a.empty? && block_given?がfalseだった場合を見てみます
public_send(*a, &b) を調べると

public_sendメソッドは、レシーバの持っているpublicなメソッドを呼び出します。引数や戻り値については、sendメソッドの説明をご覧ください。
引用:Rubyリファレンス:public_send

レシーバのpublicなメソッドに対してtry!で受け取った引数をそのまま渡しています
try!メソッドは以上です

tryメソッドに戻ります
respond_to?(a.first) を見てみます
aは可変長引数の為、配列です。 a.firstで配列の先頭の値を取得しています そして、respond_to?でレシーバに配列の先頭の値がメソッドとして実装されているかを確認しています

先程、tryの使い方を調べた際に出てきた例person.try(:name)でいうとaには[:name]が入っており、a.first:name が取れるので、 respond_to?(:name) という感じです

NilClass.try
class NilClass
  # Calling +try+ on +nil+ always returns +nil+.
  # It becomes especially helpful when navigating through associations that may return +nil+.
  #
  #   nil.try(:name) # => nil
  #
  # Without +try+
  #   @person && @person.children.any? && @person.children.first.name
  #
  # With +try+
  #   @person.try(:children).try(:first).try(:name)
  def try(*args)
    nil
  end

nilに対してtryした場合は、常にnilを返すという動きです

ついでにnil.try!も見てみると

# Calling +try!+ on +nil+ always returns +nil+.
#
#   nil.try!(:name) # => nil
def try!(*args)
  nil
end

tryと同じ動きです

tryが色々なオブジェクトで使えるのは、ActiveSupport::TryableモジュールをObjectにincludeしてからです
該当の箇所は下記になります

class Object
  include ActiveSupport::Tryable

class Delegator
  include ActiveSupport::Tryable

読んでみて

ブロックを引き受けるメソッドを作る場合の書き方が非常に参考になったので、ここらへんは活かしていきたいと思います

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

f:id:sktktk1230:20171212153754p:plain

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

読めるようにするまで

以前書いた記事に読めるようにするまでの設定を画像キャプチャ付きで解説しましたので、よろしければこちらをご参照下さい
shitake4.hatenablog.com

読んだ箇所

deep_dup を今日は読んでみようと思います

どんな使い方だっけ?

読んでみる前にまずは使い方を調べてみます
RailsガイドのActive Supportコア拡張機能の 2.4 deep_dupの項目を見てみます

deep_dupメソッドは、与えられたオブジェクトの「ディープコピー」を返します。
Rubyは通常の場合、他のオブジェクトを含むオブジェクトをdupしても、他のオブジェクトについては複製しません。
このようなコピーは「浅いコピー (shallow copy)」と呼ばれます。たとえば、以下のように文字列を含む配列があるとします。

array = ['string']
duplicate = array.dup
duplicate.push 'another-string'
# このオブジェクトは複製されたので、複製された方にだけ要素が追加された
array # => ['string']
duplicate # => ['string', 'another-string']
duplicate.first.gsub!('string', 'foo')

# 1つ目の要素は複製されていないので、一方を変更するとどちらの配列も変更される

array # => ['foo']
duplicate # => ['foo', 'another-string']

上で見たとおり、Arrayのインスタンスを複製して別のオブジェクトができたことにより、一方を変更しても他方は変更されないようになりました。
ただし、配列は複製されましたが、配列の要素はそうではありません。dupメソッドはディープコピーを行わないので、配列の中にある文字列は複製後も同一オブジェクトのままです。
オブジェクトをディープコピーする必要がある場合はdeep_dupをお使いください。例:

array = ['string']
duplicate = array.deep_dup
duplicate.first.gsub!('string', 'foo')
array # => ['string']
duplicate # => ['foo']

オブジェクトが複製不可能な場合、deep_dupは単にそのオブジェクトを返します。

number = 1
duplicate = number.deep_dup
number.object_id == duplicate.object_id # => true

引用:Active Supportコア拡張機能:2.4 deep_dup

オブジェクトをコピーしたいが、別物として利用したい場合に利用するメソッドのようです

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

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

f:id:sktktk1230:20171220113937p:plain

2. 該当箇所が3箇所だったので、それぞれみてみます

activesupport > lib > active_support > core_ext > object > deep_dup.rb
Object.deep_dup
class Object
  # Returns a deep copy of object if it's duplicable. If it's
  # not duplicable, returns +self+.
  #
  #   object = Object.new
  #   dup    = object.deep_dup
  #   dup.instance_variable_set(:@a, 1)
  #
  #   object.instance_variable_defined?(:@a) # => false
  #   dup.instance_variable_defined?(:@a)    # => true
  def deep_dup
    duplicable? ? dup : self
  end
end

まずduplicable? かどうか判定しているようです
※ duplicable?のソースコードリーディングについてはこちら
shitake4.hatenablog.com

trueの場合は dup が実行されるようです
Rubyリファレンスのclone,dupで挙動を調べてみると

cloneメソッドとdupメソッドは、レシーバのオブジェクトのコピーを作成して返します。オブジェクトのコピーとは、同じ内容を持つ別のオブジェクトです。具体的には、元のオブジェクトと同じクラスの新しいオブジェクトで、元のオブジェクトのインスタンス変数を新しいオブジェクトにコピーしたものです。

中略

浅いコピー

cloneとdupは「浅いコピー」を作ることに注意してください。
上記のCatクラスの例では、@nameがoriginalからcopiedにコピーされますが、@nameが指しているオブジェクト(文字列)は同じものです。
copiedの@nameに対して破壊的なメソッド(レシーバ自身を変更するメソッド)を呼び出すと、originalの@nameが指している文字列も変更されます。

Objectに対するdeep_dupは浅いコピーになるようです
Rubyリファレンスのclone,dupに記述している例をrails consoleで確認してみると
f:id:sktktk1230:20171220114045p:plain

originalのnameも変わっています

Array.deep_dup
class Array
  # Returns a deep copy of array.
  #
  #   array = [1, [2, 3]]
  #   dup   = array.deep_dup
  #   dup[1][2] = 4
  #
  #   array[1][2] # => nil
  #   dup[1][2]   # => 4
  def deep_dup
    map(&:deep_dup)
  end
end

レシーバに対してmapしています self.map(&:deep_dup)と同一です
(&:deep_dup) の&:は配列の各要素に対して deep_dup を実行しています
例えば、

[1, 2, 3].map(&:to_s)
=> ["1", "2", "3"]

という感じです ※詳しい解説は@kasei-san氏のこちらの記事がわかりやすいかと思います
qiita.com

ということで、配列の各要素に対してdeep_dupをおこなっています

Hash.deep_dup
class Hash
  # Returns a deep copy of hash.
  #
  #   hash = { a: { b: 'b' } }
  #   dup  = hash.deep_dup
  #   dup[:a][:c] = 'c'
  #
  #   hash[:a][:c] # => nil
  #   dup[:a][:c]  # => "c"
  def deep_dup
    hash = dup
    each_pair do |key, value|
      if key.frozen? && ::String === key
        hash[key] = value.deep_dup
      else
        hash.delete(key)
        hash[key.deep_dup] = value.deep_dup
      end
    end
    hash
  end
end

まず浅いコピーでレシーバをhashに入れています
each_pair を調べてみると

eachメソッドは、ハッシュの要素(キーと値)の数だけブロックを繰り返し実行します。繰り返しごとにブロック引数にはキーkeyと値valが入ります。each_pairメソッドは、eachの別名です。
引用:Rubyリファレンス:each, each_pair (Hash)

ということなので、レシーバのpairのkey,valueを取り出しています

次に

if key.frozen? && ::String === key
  hash[key] = value.deep_dup

を見ていきます
まずfrozen? を調べてみると

frozen?メソッドは、オブジェクトが凍結状態ならtrueを、そうでなければfalseを返します。オブジェクトを凍結状態にするには、freezeメソッドを使います。
引用:Rubyリファレンス:frozen?

更に freeze を調べてみると

freezeメソッドは、オブジェクトを凍結、つまり変更不可にします。凍結状態のオブジェクトを変更しようとすると、Ruby 1.8では例外TypeErrorが、Ruby 1.9では例外RuntimeErrorが発生します。
凍結状態を調べるには、frozen?メソッドを使います。凍結状態を元に戻すメソッドはありません。
標準クラスのオブジェクトでは、凍結したあとで破壊的なメソッド(レシーバ自身を変更するメソッド)を呼び出すと例外が発生します。
引用:Rubyリファレンス:freeze

freezeされた状態 = 破壊的変更が出来なくなっているということのようです

つまり if key.frozen? && ::String === key とは破壊的変更が出来ないStringクラスのキーの場合、valueのみをdeep_dupしています

そして

else
  hash.delete(key)
  hash[key.deep_dup] = value.deep_dup
end

はkeyが破壊的変更が可能な値の為、key,valueともにdeep_dupしています
これでコピー元のkeyに影響が無いようにしています

読んでみて

Objectでdeep_dupする場合は浅いコピーの為、気をつけなければいけないと思いました
コードを読んでみてわかることもあるので、プロジェクトでライブラリなどを導入する場合はちゃんと実装まで理解しないとバグを発生させる可能性も出てきてしまうので気をつけたいです

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

f:id:sktktk1230:20171212153754p:plain

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

読めるようにするまで

以前書いた記事に読めるようにするまでの設定を画像キャプチャ付きで解説しましたので、よろしければこちらをご参照下さい
shitake4.hatenablog.com

読んだ箇所

Active Support コア拡張機能を見ていて知った duplicable? を今日は読んでみようと思います

どんな使い方だっけ?

Railsを読んでみる前にまずは使い方を調べてみます
Active Support コア拡張機能2.3 duplicable?を読んでみると

Rubyにおける基本的なオブジェクトの一部はsingletonオブジェクトです。たとえば、プログラムのライフサイクルが続く間、整数の1は常に同じインスタンスを参照します。

1.object_id # => 3
Math.cos(0).to_i.object_id # => 3

従って、このようなオブジェクトはdupメソッドやcloneメソッドで複製することはできません。

true.dup # => TypeError: can't dup TrueClass

singletonでない数字にも、複製不可能なものがあります。

0.0.clone # => allocator undefined for Float
(2**1024).clone # => allocator undefined for Bignum

Active Supportには、オブジェクトがプログラム的に複製可能かどうかを問い合わせるためのduplicable?メソッドがあります。

"foo".duplicable? # => true
"".duplicable? # => true
0.0.duplicable? # => false
false.duplicable? # => false

デフォルトでは、nil、false、true、シンボル、数値、クラス、モジュール、メソッドオブジェクトを除くすべてのオブジェクトがduplicable? #=> trueです。
引用:Active Support コア拡張機能:2.3 duplicable?

dupメソッドやcloneメソッドで複製出来ない値が存在しているので、複製可能か確認する為の機能のようです

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

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

f:id:sktktk1230:20171218175426p:plain

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

activesupport > lib > active_support > core_ext > object > duplicable.rb
Object.duplicable?
class Object
  # Can you safely dup this object?
  #
  # False for method objects;
  # true otherwise.
  def duplicable?
    true
  end
end

上記箇所のコミットログを見てみると
f:id:sktktk1230:20171218180129p:plain

Ruby2.4以前では NilClass, FalseClass, TrueClass, Symbol, Numericdup出来ませんでしたが、2.4から可能となったようです
f:id:sktktk1230:20171218180531p:plain

処理がどう変わったのか知りたいので、Ruby2.4.0のリファレンスを見てみます

オブジェクトの複製を作成して返します。
dup はオブジェクトの内容, taint 情報をコピーし、 clone はそれに加えて freeze, 特異メソッドなどの情報も含めた完全な複製を作成します。
clone や dup は浅い(shallow)コピーであることに注意してください。後述。
TrueClass, FalseClass, NilClass, Symbol, そして Numeric クラスのインスタンスなど一部のオブジェクトは複製ではなくインスタンス自身を返します。
[PARAM] freeze:
false を指定すると freeze されていないコピーを返します。
[EXCEPTION] ArgumentError:
TrueClass などの常に freeze されているオブジェクトの freeze されていないコピーを作成しようとしたときに発生します。
引用:Ruby 2.4.0 リファレンスマニュアル:Object#clone

NilClass, FalseClass, TrueClass, Symbol, Numeric が複製ではなくインスタンス自身を返すようになったようです

合わせて以前の挙動はどうだったのか、Ruby2.3.0のリファレンスを見てみます

オブジェクトの複製を作成して返します。
dup はオブジェクトの内容, taint 情報をコピーし、 clone はそれに加えて freeze, 特異メソッドなどの情報も含めた完全な複製を作成します。
clone や dup は浅い(shallow)コピーであることに注意してください。後述。
[EXCEPTION] TypeError:
TrueClass, FalseClass, NilClass, Symbol, そして Numeric クラスのインスタンスなど一部のオブジェクトを複製しようとすると発生します。
引用:Ruby 2.3.0 リファレンスマニュアル:Object#clone

Ruby2.3.0のバージョンだとレシーバが NilClass, FalseClass, TrueClass, Symbol, Numeric の場合にTypeErrorが発生するという仕様だったんですね

該当コミット
github.com

NilClass.duplicable?
class NilClass
  begin
    nil.dup
  rescue TypeError

    # +nil+ is not duplicable:
    #
    #   nil.duplicable? # => false
    #   nil.dup         # => TypeError: can't dup NilClass
    def duplicable?
      false
    end
  end
end

Ruby2.4.0からレシーバ NilClass, FalseClass, TrueClass, Symbol, Numeric に対して dup/cloneが複製ではなくインスタンス自身を返すようになったため、オープンクラスする際にdupがTypeErrorを起こす(Ruby2.3.x以前のバージョンを使用している)場合に限りfalseを返すというようにしています

FalseClass.duplicable?
class FalseClass
  begin
    false.dup
  rescue TypeError

    # +false+ is not duplicable:
    #
    #   false.duplicable? # => false
    #   false.dup         # => TypeError: can't dup FalseClass
    def duplicable?
      false
    end
  end
end

こちらも同様です

TrueClass.duplicable?
class TrueClass
  begin
    true.dup
  rescue TypeError

    # +true+ is not duplicable:
    #
    #   true.duplicable? # => false
    #   true.dup         # => TypeError: can't dup TrueClass
    def duplicable?
      false
    end
  end
end

こちらも同様です

Numeric.duplicable?
class Numeric
  begin
    1.dup
  rescue TypeError

    # Numbers are not duplicable:
    #
    #  3.duplicable? # => false
    #  3.dup         # => TypeError: can't dup Integer
    def duplicable?
      false
    end
  end
end

こちらも同様です

Complex.duplicable?
class Complex
  begin
    Complex(1).dup
  rescue TypeError

    # Complexes are not duplicable:
    #
    #   Complex(1).duplicable? # => false
    #   Complex(1).dup         # => TypeError: can't copy Complex
    def duplicable?
      false
    end
  end
end

こちらも同様です

Rational.duplicable?
class Rational
  begin
    Rational(1).dup
  rescue TypeError

    # Rationals are not duplicable:
    #
    #   Rational(1).duplicable? # => false
    #   Rational(1).dup         # => TypeError: can't copy Rational
    def duplicable?
      false
    end
  end
end

こちらも同様です

BigDecimal.duplicable?
require "bigdecimal"
class BigDecimal
  # BigDecimals are duplicable:
  #
  #   BigDecimal.new("1.2").duplicable? # => true
  #   BigDecimal.new("1.2").dup         # => #<BigDecimal:...,'0.12E1',18(18)>
  def duplicable?
    true
  end
end

Objectと同様で常にtrueを返すようです

Method.duplicable?
class Method
  # Methods are not duplicable:
  #
  #  method(:puts).duplicable? # => false
  #  method(:puts).dup         # => TypeError: allocator undefined for Method
  def duplicable?
    false
  end
end

Methodは常にfalseを返すようです

Symbol.duplicable?
class Symbol
  begin
    :symbol.dup # Ruby 2.4.x.
    "symbol_from_string".to_sym.dup # Some symbols can't `dup` in Ruby 2.4.0.
  rescue TypeError

    # Symbols are not duplicable:
    #
    #   :my_symbol.duplicable? # => false
    #   :my_symbol.dup         # => TypeError: can't dup Symbol
    def duplicable?
      false
    end
  end
end

beginからrescueまでのコードを見てみると Ruby 2.4.0において特定のシンボルはdup出来ないようです
詳しい内容が気になったのでコミットログから探してみます
f:id:sktktk1230:20171218180338p:plain

Githubで探してみると
f:id:sktktk1230:20171218175648p:plain

.to_sym.dup するとダメな文字列があったりするようです

該当コミット
github.com

読んでみて

普段使ってなかったメソッドだったのですが、調べてみることで、詳細な仕様が把握できたため、たまに使う時などにとてもハマるということはなくなりそうだなと思いました

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

f:id:sktktk1230:20171212153754p:plain

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

読めるようにするまで

以前書いた記事に読めるようにするまでの設定を画像キャプチャ付きで解説しましたので、よろしければこちらをご参照下さい
shitake4.hatenablog.com

読んだ箇所

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

どんな使い方だっけ?

Railsの日本語ドキュメントには記載がなかったので、APIリファレンスから確認してみます

1. APIリファレンスでの探し方

http://api.rubyonrails.org/ をブラウザで開くとページの左側にsearchがあるので、そこに presence を入力します

f:id:sktktk1230:20171218113512p:plain

2. 検索結果

該当する項目があったため、クリックしてみると
f:id:sktktk1230:20171218113529p:plain

以下の説明がありました

presence()
Returns the receiver if it's present otherwise returns nil. object.presence is equivalent to
object.present? ? object : nil

For example, something like

state = params[:state] if params[:state].present?
country = params[:country] if params[:country].present?
region = state || country || 'US'

becomes
region = params[:state].presence || params[:country].presence || 'US'
@return [Object]
引用:APIリファレンス

object.present?の結果がtrueの場合は、objectそのものを返し、falseの場合は、nilを返すという仕様のようです
For exampleにあるように変数に値がある場合はその値を使用したい。ただ空の場合はデフォルト値を入れたいなどの場合に 非常にコードをシンプルに記述することが可能です

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

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

f:id:sktktk1230:20171218113555p:plain

2. 該当箇所が1箇所だったので、それを見てみます

1. activesupport > lib > active_support > core_ext > object > blank.rb
  # Returns the receiver if it's present otherwise returns +nil+.
  # <tt>object.presence</tt> is equivalent to
  #
  #    object.present? ? object : nil
  #
  # For example, something like
  #
  #   state   = params[:state]   if params[:state].present?
  #   country = params[:country] if params[:country].present?
  #   region  = state || country || 'US'
  #
  # becomes
  #
  #   region = params[:state].presence || params[:country].presence || 'US'
  #
  # @return [Object]
  def presence
    self if present?
  end

メソッドの中身を見てみると、レシーバに対して present? しており、trueであればselfを返すようです
falseの場合はnilが返り値となります
ruby最後に評価された値を返すという言語仕様な為、評価するものがなにも無い = nilということになります

※ blank?メソッドのソースコードリーディングについてはこちら

shitake4.hatenablog.com

読んでみて

丁寧なコメントが書いてあり、仕様や使い方が書いてあり非常に読みやすいと感じます

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

f:id:sktktk1230:20171212153754p:plain

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

読めるようにするまで

以前書いた記事に読めるようにするまでの設定を画像キャプチャ付きで解説しましたので、よろしければこちらをご参照下さい
shitake4.hatenablog.com

読んだ箇所

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

どんな使い方だっけ?

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

変数.present?
!blank? を実行するメソッド。if 変数.present?とunless 変数.blank?は同じ意味
nil, "", " "(半角スペースのみ), , {}(空のハッシュ) のときにfalseを返します。
Railsで拡張されたメソッドで、Rubyのみでは使えないのでご注意ください。
引用:Railsドキュメント:present

とのことです こちらも blank? 同様によく使うメソッドです

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

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

f:id:sktktk1230:20171218105833p:plain

2. 該当箇所が1箇所なので、それをみてみます

activesupport > lib > active_support > core_ext > object > blank.rb
class Object
  中略
  # An object is present if it's not blank.
  #
  # @return [true, false]
  def present?
    !blank?
  end

blank?メソッドの返り値を否定演算子で評価しています
blank?の返り値は、truefalse なので、present?も必ずtruefalse となります

※ blank?メソッドのソースコードリーディングについてはこちら

shitake4.hatenablog.com

読んでみて

今回は実装箇所が一箇所のみだったのとすでに blank? を読んでいたので、すんなりと理解することができました
読み進めていくうちにそれぞれの実装が関連しあっていくとより楽しめそうです

2回以上やることは自動化したい | SRE-SET Automation Night | イベントレポート

f:id:sktktk1230:20171207110228p:plain

2017年12月12日(火) SRE-SET Automation Nightのイベントレポートです
普段から作業の効率化や自動化といったものには非常に興味を持っているので、ここで共有して頂く知見を社内でも展開できればなと思い参加しました

イベント概要

"2回以上やることはなんでも自動化されるべきだ" Adam Stone, CEO, D-Tools
SREとSETという役割は、インフラやQAなどソフトウェア開発において手動で操作されることが多い領域を自動化するためにGoogleで作られました。このMeetupは例えば下記のような、生産性とプロダクトの品質を向上するための自動化ハックを学習し、共有し、コラボレーションするためのものです。

  • CI/CDを用いたプロダクトリリースの自動化に関する経験
  • Ansibleなどを用いたDevOpsの自動化
  • AppiumやSeleniumなどのUIオートメーションツールを使った第一印象
  • テストの自動化ハックとアドバイス
  • CircleCI, Travis, BitriseなどのクラウドCIサービスの経験
  • SeleniumやSlackbotを用いた日次タスクの自動化
  • どなたでも発表やLTしていただけますのでぜひご参加ください。

引用:[さらに増枠!] SRE-SET Automation Night

「Data processing, workflow and us ~How to manage automated jobs~」

  • Time 19:40-19:55
  • Speaker @syu_cream 氏
  • Slide

概要

BigQueryにログをアップロードする部分の話と統計情報を解析するコンポーネントの話でした

メルカリさんでは以前はこのような構成でログをBigQueryに送っていたようです f:id:sktktk1230:20171213095718p:plain

※ fluentd

ただcronジョブが失敗しやすくなったので、digdagでジョブ管理をしているようです

※ digdag

統計情報解析の話

以前の構成は f:id:sktktk1230:20171213095959p:plain

そこから Cloud Dataflowを使っているそうです

※ Cloud Dataflow
※ scio

また apache airflowも試しているとこのことでした

apache airflow

apache airflowを使って以下のようなメリットがあったようです - ジョブとジョブの依存関係を管理出来る - データ・ソースの待機が出来る

「レビューのコストを削減するための施策」

  • Time 19:55-20:10
  • Speaker @tarappo 氏

概要

レビューのコストが課題としてあったそうです
非エンジニアのQAが作ったテスト観点のドキュメントのレビューコストを減らすという内容でした

レビューサポートツールとしてDangerを使い 文書チェックツールでtextlintを使っているそうです

※ Danger
※ textlint

「After Test Automation, 自動テスト後」

  • Time 20:10-20:25
  • Speaker @vbanthia 氏
  • Slide

概要

モバイルのE2Eテストがflaky(テストが失敗する場合が色々ありすぎる)、実行環境による失敗等でメンテされずゴミ箱行きが多い
そのため、すべてを記録しましょう

※ flaky

記録する対象は f:id:sktktk1230:20171213103856p:plain

rspec_html_reporterでレポートを出力しているそうです

rspec_html_reporter

LT

「Magic Podの活用を具体的に考えてみた」

  • Time 20:30-20:35
  • Speaker 戸田広 氏
  • Slide

「Prometheusを導入した話」

  • Time 20:35-20:40
  • Speaker 株式会社Nagisa 榎戸 氏

「セキュリティ強化のための自動化」

  • Time 20:40-20:45
  • Speaker @manabusakai (freee 株式会社) 氏
  • Slide

「1人インフラ運用チームで、自動化の作業時間を確保するためにやっていること」

  • Time 20:45-20:50
  • Speaker 北野勝久( @katsuhisa__ )氏
  • Slide

TBD」Automation基盤の提供の仕方

  • Time 20:50-20:55
  • Speaker @tnir 氏

所感

第2回もあるそうなので、是非またいきたいです

Twitterハッシュタグ

#automation_night

Togetter

togetter.com