あらすじ
Ruby3.1のバージョンアップ作業に伴い多重代入の評価順序変更について影響箇所を調査する必要が出ました。
この記事では、3.0以前から3.1にアップデートしたときに具体的に何が変わったかを検証しました。
多重代入の評価順序変更とは?
以下のような多重代入について、
foo[0], bar.baz = a, b
Ruby 3.0 以前では右辺=>左辺の順序で評価されてましたが、
3.1 からは 左辺=>右辺 の評価順に変更されました。( リリースノート参照 )
検証内容
3.0 と 3.1 で同じコードを試してみて puts で実行順序を検証します。
検証1:左辺と右辺どちらが先に呼び出されるか確認
コード:
def a=_
@hoge ||= 'first called a='
end
def x
@hoge ||= 'first called x'
end
self.a, b = x, 1
puts @hoge
3.0の場合:
% ruby a.rb
first called x
3.1の場合:
% ruby a.rb
first called x
3.0 , 3.1 共に右辺から評価された
a=
b=
を呼び出す時点で右辺が評価されてないと値の代入ができないので当然といえば当然ですね。
検証2:左辺にメソッドチェインを入れてみる
左辺をオブジェクトにしてみてメソッドチェインがある時の順序を確認
コード:
class A
def initialize
puts 'A initialize'
end
def a=_
puts 'call a='
end
end
def a
return A.new
end
class B
def initialize
puts 'B initialize'
end
def b=_
puts 'call b='
end
end
def b
return B.new
end
def x
puts 'call x'
end
def y
puts 'call y'
end
a.a, b.b = x, y
3.0の場合:
% ruby a.rb
call x
call y
A initialize
call a=
B initialize
call b=
3.1の場合:
% ruby a.rb
A initialize
B initialize
call x
call y
call a=
call b=
実行順の違いが出ました。
3.0では 右辺の x
y
を評価してから右辺の a.a=
b.b=
を評価しています。
一方で3.1は左辺の a
b
を評価してから右辺の x
y
を評価し、さらに左辺に戻って a=
b=
を評価してるのがわかります。
どうやら3.1では、
左辺のセッターメソッド直前までを評価 => 右辺を評価 => セッターメソッドを評価
の順で処理が進んでるようです。
検証3:メソッドチェインが複数連なった時の挙動を確認
parent.child.method=
の時の動きも確認します。
コード:
class Parent
def initialize
puts 'initialize Parent'
end
def child
puts 'call Parent#child'
return Child.new
end
end
class Child
def initialize
puts 'initialize Child'
end
def a=_
puts 'call Child#a='
end
end
def parent
return Parent.new
end
def x
puts 'call x'
end
parent.child.a, hoge = x, 2
3.0の場合:
% ruby a.rb
call x
initialize Parent
call Parent#child
initialize Child
call Child#a=
3.1の場合:
% ruby a.rb
initialize Parent
call Parent#child
initialize Child
call x
call Child#a=
メソッドチェインが複数に連なってる時も、
左辺のセッターメソッド直前までを評価 => 右辺を評価 => セッターメソッドを評価
の順で処理が進んでるのがわかりました。
検証4:左辺が配列やハッシュの場合でも評価順が変わってるか確認する
念の為、ドットじゃないメソッドチェインでも評価順が変わってるか確認
コード:
# []= メソッド呼出時に puts するために []= を拡張する
module ArrayWrapper
def []=(a,b)
puts 'call Array#[]='
super
end
end
class Array
prepend ArrayWrapper
end
# []= メソッド呼出時に puts するために []= を拡張する
module HashWrapper
def []=(a,b)
puts 'call Hash#[]='
super
end
end
class Hash
prepend HashWrapper
end
def a
puts 'call a'
return []
end
def b
puts 'call b'
return {}
end
def x
puts 'call x'
end
def y
puts 'call y'
end
a[0], b[:b] = x, y
3.0の場合:
% ruby a.rb
call x
call y
call a
call Array#[]=
call b
call Hash#[]=
3.1の場合:
% ruby a.rb
call a
call b
call x
call y
call Array#[]=
call Hash#[]=
ドットでメソッドチェインしてない時も検証2と同様の結果になりました。
検証5:メソッドチェインがある時の左辺の呼び出し回数を確認
メソッドチェインがある時は 左辺 => 右辺 => 左辺 の順で評価されてたので、
左辺のメソッドが複数回呼び出されてないか気になったので検証
コード:
$count = 0
class A
def initialize
$count += 1
end
def a=_
end
end
def a
return A.new
end
a.a, hoge = 1, 2
puts $count
3.0は関係ないので省略
3.1の場合:
% ruby a.rb
1
aは1回しか呼び出されていないことがわかりました。
そもそも検証2の時点で A initialize
が1回しか呼び出されてないので当然といえば当然ですね。
まとめ
- 左辺でメソッドチェインを行なってない場合は、 3.1 と 3.0 以前で挙動の変化はない
- 3.1では左辺でメソッドチェインがある場合、
左辺のセッターメソッド直前までを評価 => 右辺を評価 => セッターメソッドを評価
の順で処理が進んでる。 - 3.1 で 左辺 => 右辺 => 左辺 の順で処理が進んでも、左辺の処理が重複して実行されることはない