Edited at

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

More than 3 years have passed since last update.

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

この記事は 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 が値を返してしまうため,代入は行われないわけだ。