わさっきhb

大学(教育研究)とか ,親馬鹿とか,和歌山とか,とか,とか.

nilガード

Rubyにおいて,「||=」演算子を使うことの意義は分かるし,自分でもよく使うのですが…

他人のコードを見たRuby初心者の多くは,この風変わりなイディオムに困惑する.

a ||= []

これは,aの値が空の配列になるかもしれないという意味だ.||=は以下を省略した構文である.

a = a || []

このコードを理解するには,このOR演算子(||)について詳しく理解する必要がある.||演算子は,被演算子のいずれかがtrueであれば,trueを返す.
nilとfalse以外の値はすべてtrueになることを覚えておいてほしい.最初の被演算子がtrueなら,||演算子はtrueを返す.falseなら,2番目の被演算子を返す.これはつまり,被演算子がどちらもfalseでない限り,結果はtrueになるということだ.これは,いわゆるOR演算子の考えと同じだ.
ということは,先ほどのコードは以下のコードと同じだと考えられる.

if a != nil
  a = a
else
  a = []
end

このコードは以下のように翻訳できる.「もしaがnilでないならば,そのままにする.nilならば,空の配列にする.」熟練Rubyプログラマは,ifよりも||演算子のほうが優雅で読みやすいと考えるようだ.このイディオムは配列以外にも使える.変数がnilにならないようにするガード(見張り役)なので,このイディオムはnilガードと呼ばれる.
nilガードはインスタンス変数を初期化するのにもよく使われる.(略)
メタプログラミングRuby*1, p.268)

上の説明,間違いが多数,目について困ります.

  • 「a ||= []」*2は「a = a || []」ではなく,「a || (a = [])」です.リファレンスマニュアルの「演算子式」で確認できます*3
  • 「1 || true」はtrueではなく1と評価されます*4.これは,「||演算子は,被演算子のいずれかがtrueであれば,trueを返す」と「被演算子がどちらもfalseでない限り,結果はtrueになる」に対する反例になります.
  • nilとfalse以外の値はすべてtrueになる」って,そんなことはありません.ここの「true」は「真」なのでしょう.全体的に,「定数true」と「真となる値」の区別がついていない印象があります.
  • ifを使った代替コードの「if a != nil」は「if a != nil && a != false」または「if a」とし,次の行の「a = a」は単に「a」とすべきです.

言語仕様として,「a ||= []」は「a = a || []」ではなく「a || (a = [])」となっているのでそこまでなのですが,確認のコードを書いてみました.

#!/usr/bin/env ruby
# -*- coding: utf-8 -*-

# sample.rb

class Sample
  def initialize
    @x = nil
  end

  def x
    puts "インスタンス変数@xを参照しました"
    @x
  end

  def x=(value)
    puts "インスタンス変数@xに#{value}を代入しました"
    @x = value
  end
end

if __FILE__ == $0
  s = Sample.new
  puts "s.x ||= []  1回目"
  s.x ||= []
  puts "s.x ||= []  2回目"
  s.x ||= []
  puts "s.x = s.x だと?"
  s.x = s.x
end

「変数aの値を参照する」ことや「変数aに代入を行う」ことを検出するような,Rubyのコードというのは,ちょっと難しそうです.また,=と||は再定義できない演算子である点にも,留意しないといけません.ですが,独自に定義したクラスのインスタンスsに対して,「s.xの値を参照する」ことや「s.xに代入を行う」ことはメソッドとして記述ができ,その中にputによる出力処理を書いておけば,参照・代入がどのタイミングでなされるか,目に見えて分かるという次第です.
上のプログラムで,実行前に考えることは次の2つです.
「s.x ||= []」が「s.x = s.x || []」と等価であれば,

s.x ||= []  1回目
インスタンス変数@xを参照しました
インスタンス変数@xに[]を代入しました
s.x ||= []  2回目
インスタンス変数@xを参照しました
インスタンス変数@xに[]を代入しました
s.x = s.x だと?
インスタンス変数@xを参照しました
インスタンス変数@xに[]を代入しました

という出力になります.
そうではなく,「s.x ||= []」が「s.x || (s.x = [])」と解釈されるのなら,

s.x ||= []  1回目
インスタンス変数@xを参照しました
インスタンス変数@xに[]を代入しました
s.x ||= []  2回目
インスタンス変数@xを参照しました
s.x = s.x だと?
インスタンス変数@xを参照しました
インスタンス変数@xに[]を代入しました

です.実際に,いくつかの環境(1.8,1.9)で実行してみると,出力はすべて後者でした.

*1:http://ascii.asciimw.jp/books/books/detail/978-4-04-868715-7.shtml

*2:はてなダイアリー記法と衝突するので,地の文では,ブラケット記号をマルチバイト文字で表現しています.

*3:http://doc.okkez.net/static/192/doc/spec=2foperator.html, http://doc.okkez.net/static/187/doc/spec=2foperator.html, http://www.ruby-lang.org/ja/man/html/_B1E9BBBBBBD2BCB0.html

*4:「2||1」は,Rubyでは2で,Cでは1です.Cの場合は,論理OR演算子の評価結果は0か1のいずれかになるからです.「2|1」は,RubyでもCでも3です.ともに|は,ビットOR演算子だからです.