ブロック引数つきのObject#presenceというのを考えた

以下のような処理をしたい。name という名前のユーザを探し、その年齢を出力する。

def age(name)
  Persion.where(:name => name).age
end

上のコードはまずいところが一箇所ある。 name というユーザがいなかった場合に、例外になるという点だ。

def age(name)
  Persion.where(:name => name).age # => NilClass#age is not defined
end

これを回避するためには、nilチェックを入れる必要がある。もし name というユーザがいなかったら、nilを返すことにする。

def age(name)
  person = Persion.where(:name => name)
  person ? person.age : nil
end

単に年齢を取得したいだけなのに、中間変数 person を用意しないといけない。もっと簡潔に書けないだろうか?

ActiveSupport の Object#presence

ところで、ActiveSupportにはObject#presenceというのがある。中身はこうだ。

class Object
  def presence
    self if present?
  end
end

もっと分かりやすく書きなおすと、こうだ。

class Object
  def presence
    return present? ? self : nil
  end
end

present? というのは blank? の真偽を逆にしたメソッド。つまり、空文字、空配列、nil、falseなどのときにfalseを返し、それ以外はtrueを返す。
このObject#presenceは何に使うのかというと、

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

と書くようなところを

region = params[:state].presence || params[:country].presence || 'US'

とシンプルに書けるようになる。

ブロック引数付きのObject#presenceを作る

本題に戻る。以下の記述をシンプルにしたいという問題。

def age(name)
  person = Persion.where(:name => name)
  person ? person.age : nil
end

これを、次のように書くというのを考えた。

def age(name)
  Persion.where(:name => name).presence(&:age)
end

分かりやすく書き直すと、こう

def age(name)
  Persion.where(:name => name).presence {|person| person.age}
end

どういう仕組みかというと、Object#presenceがブロック引数を受け取れるように拡張した。

require 'active_support/core_ext/object/blank'
class Object
  alias _orig_presence presence
  def presence
    present? ? (block_given? ? yield(self) : self) : nil
  end
end

もしブロック引数があれば、present?なときにyield(self)を返す。ブロック引数がなければ本来のpresenceの動作をする。
ちょっとややこしいけど、この拡張を導入することでスッキリ書けるシーンがちょいちょいあって気分良くなりました。