情報を返す問い掛けメソッド

Rubyでメソッド名の最後に?マークが付いたものがあります。この種のメソッドは問い掛けメソッドであり、通常、trueまたはfalseのbool値を返すよう実装されています。

12.even? # => true
list.empty? # => false
path.file? # => true

しかしその中には、単にbool値を返すだけでなく、その結果の情報を返してくれるメソッドもあります。例えば、Encoding.compatible?です。

str = 'hello'
str.encoding # => #<Encoding:UTF-8>
str2 = "\xa1".force_encoding("iso-8859-1")
str2.encoding # => #<Encoding:ISO-8859-1>
str3 = "\xa1".force_encoding("euc-jp")
str3.encoding # => #<Encoding:EUC-JP>

Encoding.compatible?(str, str2) # => #<Encoding:ISO-8859-1>
Encoding.compatible?(str2, str3) # => nil

str4 = str + str2
str4.encoding # => #<Encoding:ISO-8859-1>

渡された2つの文字列にエンコーディング互換性がない場合はnilを返しますが、互換性がある場合には、それらが結合されたときの文字列のエンコーディング情報を文字列で返します。

それから、これは正確にはメソッドではありませんが、defined?があります。

val, $val, VAL = 1, 2, 3
def meth
  :hi
end

defined? val # => "local-variable"
defined? $val # => "global-variable"
defined? VAL # => "constant"
defined? meth # => "method"
defined? 10 # => "expression"
defined? x # => nil

defined?は任意の式を受け取って、その式が定義されていなければnilを返しますが、定義されている場合には、その式の種類を返します。

Rubyでは、falseとnil以外はすべてがtrueを意味するので、これらのメソッドは依然としてbool値を返すメソッドとして、うまく機能するのです。

falseのときに情報を返すメソッド

一方で、Rubyではメソッドがfalseを返すとき、同時に情報を受け渡すことができません。falseとnil以外のオブジェクトはすべてtrueと判断されてしまうからです。

一般社会では、通常、リクエストに対する答えがNOのときにこそ、その理由がほしいものです。例えば..

「社長、昇格してほしんだけど。」 => 「駄目だ!」
「俺と付き合ってよ?」 => 「はっ?」

そんな答えじゃ納得いきません。なぜダメなのか、断る理由こそが重要なのです!


そんな背景から、Rubyにおいてfalseが返されるときに、その理由などの情報を同時に返す方法を考えてみました ^ ^;

その方法とは…

FalseClass#to_sを使うのです1

FalseClass#to_sにはdefaultで文字列"false"がセットされていますが、使い道はあまり無さそうです。そこで、これを使って情報を渡せるように再定義します。例を示します。

class Movie < Struct.new(:title, :r18)
  def can_watch?(who)
    return true unless r18
    (who.age > 18).tap { |bool| set_reason(bool, who) }
  end

  private
  def set_reason(bool, who)
    t = title
    bool.class.instance_eval { remove_method :to_s }
    bool.define_singleton_method(:to_s) { "`#{t}` is for adult. and you are #{who.age}." }
  end
end

Movieクラスは、引数に渡されたユーザがその映画を見ることができるかを尋ねるcan_watch?メソッドを持っています。can_watch?メソッドは、その映画がR-18指定のとき、ユーザの年齢に応じてtrueまたはfalseを返します。それと同時に、その結果に応じて、その理由に係る文字列を返すFalseClass#to_sまたはTrueClass#to_sを再定義します。

さあ、このクラスを使って、CharlieBobが目的の映画を見られるか判断してみましょう。

movie = Movie["しんちゃんのプロレスごっこ", true] # => #<struct Movie title="しんちゃんのプロレスごっこ", r18=true>

Person = Struct.new(:name, :age)

charlie = Person[:Charlie, 13]  # => #<struct Person name=:Charlie, age=13>
bob = Person[:Bob, 21] # => #<struct Person name=:Bob, age=21>

[charlie, bob].each do |p|
  puts "#{p.name}'s result:"
  if reason = movie.can_watch?(p)
    puts "  Yes, you can! (reason: #{reason})"
  else
    puts "  No, you can't! (reason: #{reason})"
  end
  puts
end

# >> Charlie's result:
# >>   No, you can't! (reason: `しんちゃんのプロレスごっこ` is for adult. and you are 13.)
# >> 
# >> Bob's result:
# >>   Yes, you can! (reason: `しんちゃんのプロレスごっこ` is for adult. and you are 21.)
# >>

結果がfalseのときも、見事にその理由が返ってきました。これなら断られても納得できますね!


断る力 (文春新書) by 勝間 和代


(追記:2012-6-28) サンプルコードにremove_methodを追加しました。

参考: モジュールをオープンしてメソッドを上書きする - わからん

  1. もちろんネタですよ..


blog comments powered by Disqus
ruby_pack8

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