CoffeeScriptには存在演算子(Existential Operator)なるものがあるそうだよ。これは変数が存在するかをチェックするものなんだ。

# coffee
charlie = {name:'Charlie', age:50}

charlie.age -= 30 if timeMachine?
charlie.age # =>  50

timeMachine = 'YES'
charlie.age -= 30 if timeMachine?
charlie.age # =>  20

charlie.hobby ?= 'programming'

charlie.hobby # =>  programming

Rubyにはそのような演算子は無いけれども、nil?とかdefined?とか||=とかを使えば同等のことができるよね。

# ruby
User = Struct.new(:name, :age, :hobby)
charlie = User['Charlie', 50]

charlie.age -= 30 if defined?(time_machine)
charlie.age # => 50

time_machine = 'YES'
charlie.age -= 30 if defined?(time_machine)
charlie.age # => 20

charlie.hobby ||= 'programming'
charlie.hobby # => "programming"

でもね、CoffeeScriptの存在演算子はもう一歩先を行ってるんだよ。なんてったってプロパティチェーンの中でも使えちゃうんだから。

# coffee
charlie.scores = {math:35, english:78, music:60}

charlie.scores?.math # => 35

liz = {name:'Liz', age:17}

liz.scores.math # => TypeError: Cannot read property 'math' of undefined
liz.scores?.math # => undefined

lizの結果を見てもらえばわかると思うけど、存在演算子を使えばscoresが未定義でもエラーを吐かずに、出力を一つ前(scores)の返り値に戻せるんだ。

残念ながらRubyにはこれができる演算子やメソッドは無いんだよ。だから普通はみんなエラーを避けるため、途中結果を一旦変数に取って場合分けしてると思うんだ。

# ruby
User = Struct.new(:name, :age, :hobby, :scores)
charlie = User['Charlie', 50]

charlie.scores = { math:35, english:78, music:60 }

charlie.scores[:math] # => 35

liz = User['Liz', 17]

liz.scores[:math]
# ~> -:17:in `<main>': undefined method `[]' for nil:NilClass (NoMethodError)

liz_scores = liz.scores
liz_scores ? liz_scores[:math] : nil # => nil

でも、それってスマートじゃないよね?悲しいよね?

Rubyに存在メソッドを定義する

そんなわけで…

CoffeeScriptの存在演算子に対抗すべく、Rubyで存在メソッドObject#al?っていうのを考えてみたよ。all?じゃないからね。

class Object
  def al?
    return !!self unless block_given?
    yield self if self
  end
end

charlie.scores.al?{ |sco| sco[:math] } # => 35
liz.scores.al? { |sco| sco[:math] } # => nil

charlie.scores.al? # => true
liz.scores.al? # => false

al?はブロックを取るよ。で、receiverがnilならnilを返すけど、そうでなければreceiverを引数にとってブロックの実行結果を返すんだ。ブロックがなければブール値を返すよ。

aliasもあるよ。

# encoding: UTF-8
alias :ある? :al?

charlie.scores.ある?{ |sco| sco[:math] } # => 35
liz.scores.ある? { |sco| sco[:math] } # => nil

日本人のみんなはこっちを使えばいいと思うよ。

Ruby2.0で採用間違いなし!と思うんだけど、みんなはどう思う?

Object#tapを使う

って、これじゃネタ止まりだよね..僕らにはもっと現実的な解決策が必要だよ。

で、いい方法を思いついたんだよ。

Object#tapを使うんだ。そう僕らにはtapがあるんだよ!

charlie.scores.tap{ |s| break s[:math] if s } # => 35
liz.scores.tap{ |s| break s[:math] if s } # => nil

scoresがあるときはbreakでブロックの結果を返すけど、scoresがnilのときはtapの挙動通りブロックの結果は無視されてsocoresつまりnilが返るよ。ブロック中のbreakがミソだよ。

ちょっと冗長だけど、条件分岐よりはいいよね?

もちろんtap+breakは、存在確認以外のことにも使えるよ。

charlie.name.tap { |n| break n.center(10, '*').upcase if n.size < 7 } # => "Charlie"
liz.name.tap { |n| break n.center(10, '*').upcase if n.size < 7 } # => "***LIZ****"

この例では名前の長さを分岐の条件にしてるよ。

やっぱりRubyはおもしろいよね。

知ってるネタだったら、ゴメンね。


(追記:2012-10-31)

@yayuguさんにブコメで、RailsのActiveSupportにObject#try, NilClass#tryなるものがあることを教えてもらったよ。

require "active_support/all"

charlie.scores.try { |s| s[:math] } # => 35
liz.scores.try { |s| s[:math] } # => nil

折角だから実装も見てみるね。

class Object
  def try(*a, &b)
    if a.empty? && block_given?
      yield self
    else
      __send__(*a, &b)
    end
  end
end

class NilClass
  def try(*args)
    nil
  end
end

NilClass#nilの実装がなんとも言えないねー。

Object#tryでは引数渡しもできるようになってるんだね。

charlie.scores.try(:[], :math) # => 35
liz.scores.try(:[], :math) # => nil

その場合はsendに移譲するから引数は必須だよ。

一方、Object#al?ではCoffeeScriptの存在演算子同様、無引数の場合はBool判定するから、この部分の挙動がちょっと違うんだ。

class Object
  def al?
    return !!self unless block_given?
    yield self if self
  end
end

charlie.scores.al?{ |sco| sco[:math] } # => 35
liz.scores.al? { |sco| sco[:math] } # => nil

charlie.scores.al? # => true
liz.scores.al? # => false

tryにもこの機能があっていいように思うんだけどどうなのかな。


進化の存在証明 by リチャード・ドーキンス and Richard Dawkins



blog comments powered by Disqus
ruby_pack8

100円〜で好評発売中!
M'ELBORNE BOOKS