nil?すべきか

Rubyを使っているとコードをより簡潔により美しくしたいという欲求、つまりDRY欲が加速します。

例えば次のようなコードがあります。ここでの関心はprocess_userメソッドです。

class String
  def some_process
    "Process_completed for %s" % self
  end
end

def process_user
  unless @user
    @user = 'anonymous'
  end
  @user.some_process
end

@user = nil
process_user # => "Process_completed for anonymous"

@user = 'Charlie'
process_user # => "Process_completed for Charlie"

Rubyistはこのコードを見てムズムズします。

そしてunless修飾子を使ってこんな風にリファクタします。

def process_user
  @user = 'anonymous' unless @user
  @user.some_process
end

@user = nil
process_user # => "Process_completed for anonymous"

@user = 'Charlie'
process_user # => "Process_completed for Charlie"

いえいえ、OR演算子の短絡を利用して、こんな風にリファクタします。

def process_user
  @user = @user || 'anonymous'
  @user.some_process
end

@user = nil
process_user # => "Process_completed for anonymous"

@user = 'Charlie'
process_user # => "Process_completed for Charlie"

いやいや、自己代入演算子でこんな風にリファクタします。

def process_user
  @user ||= 'anonymous'
  @user.some_process
end

@user = nil
process_user # => "Process_completed for anonymous"

@user = 'Charlie'
process_user # => "Process_completed for Charlie"

empty?すべきか

@userがnilでなく空文字を受ける場合はどうでしょう。String#empty?を使った最初のコードは次のようになります。

def process_user
  if @user.empty?
    @user = 'anonymous'
  end
  @user.some_process
end

@user = ""
process_user # => "Process_completed for anonymous"

@user = 'Charlie'
process_user # => "Process_completed for Charlie"

ムズムズするので、if修飾子を使ってリファクタします。

def process_user
  @user = 'anonymous' if @user.empty?
  @user.some_process
end

@user = ""
process_user # => "Process_completed for anonymous"

@user = 'Charlie'
process_user # => "Process_completed for Charlie"

まだムズムズするので、OR演算子を使ってリファクタします。

def process_user
  @user = !@user.empty? || 'anonymous'
  @user.some_process
end

@user = ""
process_user # => "Process_completed for anonymous"

@user = 'Charlie'
process_user # => 
# ~> -:9:in `process_user': undefined method `some_process' for true:TrueClass (NoMethodError)
# ~> 	from -:16:in `<main>'

ところがこれはエラーになります。@userが空文字でなければ、@userにtrueがセットされてしまうからです。

それが問題だ

そこでこんな対策を考えました。空文字のときはnilを返しそうでないときはselfを返すString#to_nilを定義するのです。

class String
  def to_nil
    self unless empty?
  end
end

そしてString#empty?の代わりに使います。

def process_user
  @user = @user.to_nil || 'anonymous'
  @user.some_process
end

@user = ""
process_user # => "Process_completed for anonymous"

@user = 'Charlie'
process_user # => "Process_completed for Charlie"

良いアイディアだと思ったのですが、そんなものはRails界隈でとうの昔にありました。そう、Object#presenceです。Railsを知らない人は困ります > 私^ ^;

require "active_support/all"

def process_user
  @user = @user.presence || 'anonymous'
  @user.some_process
end

@user = ""
process_user # => "Process_completed for anonymous"

@user = 'Charlie'
process_user # => "Process_completed for Charlie"

因みに実装は次のとおりです。

class Object
  def presence
    self if present?
  end

  def present?
    !blank?
  end

  def blank?
    respond_to?(:empty?) ? empty? : !self
  end
end

これに倣って、私のString#to_nilもObject#to_nilに昇格させます。こんな感じでしょうか。

class Object
  def to_nil
    self if respond_to?(:empty?) && !empty?
  end
end

ここまで来ると、DRY浴が止まりません。

nilのときのように自己代入させたいです。

def process_user
  @user.to_nil ||= 'anonymous'
  @user.some_process
end

@user = ""
process_user # => 

@user = 'Charlie'
process_user # => 
# ~> -:15:in `process_user': undefined method `to_nil=' for "":String (NoMethodError)
# ~> 	from -:20:in `<main>'

当然エラーになります。でもto_nil=が無いと言っていますので、これを定義してみます。

class Object
  def to_nil
    self if respond_to?(:empty?) && !empty?
  end
  
  def to_nil=(obj)
    replace(obj) if respond_to?(:replace)
  end
end

def process_user
  @user.to_nil ||= 'anonymous'
  @user.some_process
end

@user = ""
process_user # => "Process_completed for anonymous"

@user = 'Charlie'
process_user # => "Process_completed for Charlie"

うまくいきました。自己代入では最初にto_nilが呼ばれてselfか’anonymous’が返り、次にto_nil=がこの返り値とともに呼ばれてselfをその値でreplaceします。

他のオブジェクトに対しても試してみます。

str1, str2, arr1, arr2, hash1, hash2, nil1 = 'hello', '', [1,2,3], [], {a:1, b:2}, {}, nil

str1.to_nil ||= 'default'
str2.to_nil ||= 'default'
arr1.to_nil ||= [:default]
arr2.to_nil ||= [:default]
hash1.to_nil ||= {default: 1}
hash2.to_nil ||= {default: 1}
nil1.to_nil ||= 'default'

str1 # => "hello"
str2 # => "default"
arr1 # => [1, 2, 3]
arr2 # => [:default]
hash1 # => {:a=>1, :b=>2}
hash2 # => {:default=>1}
nil1 # => nil

当然ながら、nil1の結果だけは期待通りになりません。nilをreplaceできたら面白いかもしれません。

まあちょっと奇妙なコードです。やり過ぎ感が漂います。

そんなわけで、楽しいGWを!

ハムレット (新潮文庫) by ウィリアム シェイクスピア



blog comments powered by Disqus
ruby_pack8

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