そういうこともある

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

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

読んでみて

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