以下のような処理をしたい。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の動作をする。
ちょっとややこしいけど、この拡張を導入することでスッキリ書けるシーンがちょいちょいあって気分良くなりました。