LoginSignup
67

More than 5 years have passed since last update.

Ruby の自己代入 x ||= 1 の謎

Last updated at Posted at 2015-01-12

この記事にはいくつか誤りがあることが分かったので,全面的に書き直す予定です。ごめんなさい。

この記事は Ruby 初心者向け。

Ruby で,x にマトモな値が入っていない場合だけ何かを代入したい,というときに

x ||= 1

とかって書くよね。「マトモな値」というのは,ここでは〈真〉と評価される値のことを指している。

ちなみに Ruby では nil と false 以外のすべてのオブジェクトが〈真〉と評価される。

解釈

この文は

x || (x = 1)

と解釈されることになっている。

読者の中には,「えっ?

x = x || 1

じゃないの?」と思う人もいるかもしれないけど,そうじゃない。その話はあとでするね。

|| という演算子

|| という二項演算子は,OR 条件の論理演算を行う。

式1 || 式2 を評価した結果は,式1式2 の少なくとも一方が〈真〉のときに〈真〉となり,式1式2 が両方とも〈偽〉のときに〈偽〉となる。

こんな疑問が湧かないかな?

でもさ,評価結果が「〈真〉となる」とか「〈偽〉となる」とか言ったってさ,具体的には何になるのさ?

もっともだ。Ruby では式の値はすべて何らかのオブジェクトになるわけだから,どういうオブジェクトなんだよ?っていう疑問だね。

じゃあ,言い直そう。

式1 || 式2 を評価した結果は,式1 が〈真〉のときは 式1 の評価結果となり,式1 が〈偽〉のときは 式2 の評価結果となる。

ちょっと待てよ,さっきと全然違うじゃねーか。

いや,言い方は違うけど,これが OR 条件の評価になっていることは,式1式2 に nil だの 3.14 だのを入れて真/偽の組合せをいろいろやってみれば分かる。

# 偽 OR 偽 → 偽 の例
puts nil || false #=> false

# 偽 OR 真 → 真 の例
puts false || :hoge #=> :hoge

# 真 OR 偽 → 真 の例
puts "" || nil #=> ""

# 真 OR 真 → 真 の例
puts [] || 3.14 #=> []

実行

では

x || (x = 1)

がどのように実行されるかを見てみよう。

x が〈偽〉と評価される値を持っている場合,x = 1 が評価される。これによって x1 が代入されるわけだ。

この場合,式全体の評価値は 1 となる。なぜなら式 x = 1 の評価値が 1 だからだ。あ,ちなみに Ruby では代入も〈式〉の一種で,値を持つんだからね。

一方,x が〈真〉と評価される値を持っている場合,x = 1 は評価しない。そして式全体の評価値は x の評価値となる。

x が未定義の場合

実は x1 が代入されるのは,x が偽のときだけではない。x が未定義の場合も代入が行われる。

だから,

x ||= 1
puts x

とだけ書いたスクリプトは,期待通り 1 を表示してくれる。

しかし,あなたはまたこんな疑問を持つかもしれない:

x が未定義だったら x || (x = 1) を評価しようとして NameError を出すはずでは?

その疑問ももっともだ。その前に,NameError って何だったっけ。

NameError

x が未定義のときに

puts x

を実行しようとすると,

NameError: undefined local variable or method `x' for main:Object

みたいなエラーが出る。

Ruby の処理系にとって,x はとりあえず〈識別子〉であるわけだ。小文字で始まっているので,可能性としては①ローカル変数の参照と,②メソッド呼出しの二つが考えられる。

※Ruby は引数無しでメソッドを呼び出すとき,( ) を省略してメソッド名だけを書いてもよいので,puts xx は,メソッド x を呼び出しているの かも しれないのだ。

しかし,x というメソッドが定義されていなくて,x というローカル変数も知らない場合はエラーとなる。これが「undefined local variable or method」というメッセージの意味するところだ。

なぜ NameError にならないのか

この章には誤りがあるかもしれません。あとで調べます。次章には誤りがあります。

Ruby では,最初の代入式が現れる箇所以降でそのローカル変数が定義済みとみなされる。

「んなこと,たりめーじゃねーか」だって?

ところが,必ずしも当たり前じゃないんだな。次のスクリプトを見てくれ。

if false
  x = 3
end
puts x #=> nil

x = 3 は決して評価されない。にも関わらず puts x は NameError を出さない。

というのは,if 文のところで x に代入する代入式が現れるので,この時点で x という識別子は Ruby の処理系にとって〈見知らぬ者〉ではなくなるらしい。定義済みローカル変数になるのだ。

puts x を実行するとき,x は定義済みだ。しかし代入はされていない。こういうとき x は値 nil を持つ。そういう仕様だ。

〈if 修飾子〉ってやつを使って

x = 3 if false
puts x #=> nil

と書いても同じこと。

再び x || (x = 1)

この章には誤りがあります。あとで調べて書き直します。

ではもう一度

x || (x = 1)

を見てみよう。

Ruby の処理系はこいつの言わんとするところをまず解釈し,そしておもむろにまず左側の x を評価しようとする。

筆者も実は理解が怪しいのだが,Ruby の処理系はこの式を解釈するときに x = 1 を見ているので,x を評価する際にこいつがローカル変数だということを知っているようなのだ。 この箇所が誤りです。あとで書き直します。

だから NameError は出さず,x を評価することができる。結果は nil だ。

nil は偽なので,次に x = 1 を評価する。かくして x1 が代入されることになる。

実はもっと単純な

x = x

でも同様のことが確かめられる。

こいつは x が未定義でも NameError にはならない。未定義の場合は x の値は nil になる。

どうも,この代入式を解釈するときに,識別子 x が代入の左辺にあることからローカル変数と認識されるようだ。そして代入の右辺を評価すると nil になるので,x に nil が代入される,ということらしい。

自己代入

さて,この記事のタイトルにも掲げた

x ||= 1

という書き方に戻ろう。

こういうやつは「自己代入」と呼ばれている。

自己代入にはいろいろな仲間がいるけど,いずれも

1 演算子= 2

という形をしている。

この〈演算子〉の部分には

+ - * / % ** & | ^ << >> && ||

が入る。これらは二つのグループに分かれる。

+ から >> までは

1 = 1 演算子 2

と解釈される。よって,たとえば

x += 1

x = x + 1

と解釈され,x の値を 1 増やすことになる。

残りは &&|| で,これらは

1 演算子 (1 = 2)

と解釈される。

だから,

x ||= 1

x || (x = 1)

になるわけだ。

x || (x = 1)x = x || 1 は同じ?

&&|| の場合だけ自己代入の定義が違うのが気になるね。

以下の二つを比較してみよう。

x || (x = 1)
x = x || 1

どちらの場合も,x が未定義または〈偽〉のときだけ代入が行われ,x が〈真〉のときは値は変わらない。式全体の値も常に一致する。

違いと言えば,x が〈真〉のとき,前者は代入が行われないが,後者は行われる(自分自身の値が代入される)こと。

んー,だけど,自分の値が自分に入るんじゃ,代入しないのと同じなんでは? 結局,違いなんて無いんじゃねーの?

しかし,次の例はどうだろう。

h = Hash.new("default")

h[:foo] = h[:foo] || "foo"

h[:bar] || (h[:bar] = "bar")

p h #=> {:foo=>"default"}

まず,思い出してほしいんだけど,Ruby のハッシュのデフォルト値というのは,知らないキーで値を参照した場合に返す値のことだ。

さて,上のスクリプトで,式1 = 式1 || 式2 の形だと,式1 が〈真〉であっても代入は行われ,ハッシュにキーが追加されることになる。

しかし,式1 || (式1 = 式2) の形だと 式1 が〈真〉の場合には代入が行われないので,ハッシュは変化しない。

だから,デフォルト値を持っているハッシュで,知らないキーを使って

some_hash_with_default[:unknown_key] ||= 1

とやっても,代入は行われず,ハッシュは変化しない。

これ,ちょっとした落とし穴だよね。

同じようなことは,オブジェクトの〈属性〉の参照/代入についても言える。

class C
  def hoge
    @hoge || "知らん"
  end

  def hoge=(value)
    @hoge=value
  end
end

c=C.new
c.hoge ||= "知っとる"
p c.hoge #=> "知らん"

インスタンス変数 @hoge が nil でも c.hoge が値を返してしまうため,代入は行われないわけだ。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
67