🌟
Ruby の Refinements の挙動に少しハマった話
先日、deep_compact
という gem を作り、リリースしました。
README にもある通り、以下のように using DeepCompact
と宣言することで、
Array#deep_compact
と Hash#deep_compact
がそれぞれ定義されます。
#deep_compact
は、対象のオブジェクトの子要素に対し、再帰的に #compact
を適用します。
require 'deep_compact'
using DeepCompact
array = [nil, :a, { a: :a, nil: nil, array: [nil, :a] }]
pp array.deep_compact #=> [:a, {:a=>:a, :array=>[:a]}]
hash = { a: :a, nil: nil, array: [nil, :a], hash: { a: :a, nil: nil } }
pp hash.deep_compact #=> {:a=>:a, :array=>[:a], :hash=>{:a=>:a}}
問題のコード
現在のコードは、以下のとおり、「1つのモジュールで 2つのクラスを再定義する」実装となっています。
lib/deep_compact.rb
lib/deep_compact.rb
module DeepCompact
refine ::Array do
def deep_compact
compact.map do |value|
value.respond_to?(:deep_compact) ? value.deep_compact : value
end
end
end
refine ::Hash do
def deep_compact
compact.transform_values do |value|
value.respond_to?(:deep_compact) ? value.deep_compact : value
end
end
end
end
モジュールを分割したかった
これを以下のように「2つのモジュールでそれぞれ 1つのクラスを再定義する」形に書き直すと、少し挙動が変わります。
lib/deep_compact.rb
lib/deep_compact/array.rb
module DeepCompact
module Array
refine ::Array do
def deep_compact
compact.map do |value|
value.respond_to?(:deep_compact) ? value.deep_compact : value
end
end
end
end
end
lib/deep_compact/hash.rb
module DeepCompact
module Hash
refine ::Hash do
def deep_compact
compact.transform_values do |value|
value.respond_to?(:deep_compact) ? value.deep_compact : value
end
end
end
end
end
lib/deep_compact.rb
require_relative 'deep_compact/array'
require_relative 'deep_compact/hash'
module DeepCompact
include Array
include Hash
end
require 'deep_compact'
using DeepCompact
array = [nil, :a, { a: :a, nil: nil, array: [nil, :a] }]
pp array.deep_compact #=> [:a, {:a=>:a, nil: nil, :array=>[nil, :a]}]
hash = { a: :a, nil: nil, array: [nil, :a], hash: { a: :a, nil: nil } }
pp hash.deep_compact #=> {:a=>:a, :array=>[nil, :a], :hash=>{:a=>:a}}
何が起きているのか
-
Array#deep_compact
、Hash#deep_compact
はそれぞれ定義される、ただし-
Array#deep_compact
の実行スコープ内ではHash#deep_compact
が定義されていない -
Hash#deep_compact
の実行スコープ内ではArray#deep_compact
が定義されていない
-
-> Array に含まれる Hash 内の nil
要素、Hash に含まれる Array 内の nil
要素、がそれぞれ削除されない
「じゃあ」、と
-
refine Array
のブロック内でusing ::DeepCompact::Hash
を -
refine Hash
のブロック内でusing ::DeepCompact::Array
を
それぞれ宣言すると、以下の通りエラーが発生します。
lib/deep_compact.rb
lib/deep_compact/array.rb
module DeepCompact
module Array
refine ::Array do
using ::DeepCompact::Hash
def deep_compact
compact.map do |value|
value.respond_to?(:deep_compact) ? value.deep_compact : value
end
end
end
end
end
lib/deep_compact/hash.rb
module DeepCompact
module Hash
refine ::Hash do
using ::DeepCompact::Array
def deep_compact
compact.transform_values do |value|
value.respond_to?(:deep_compact) ? value.deep_compact : value
end
end
end
end
end
lib/deep_compact.rb
require_relative 'deep_compact/array'
require_relative 'deep_compact/hash'
module DeepCompact
include Array
include Hash
end
$ irb
irb(main):001:0> require 'lib/deep_compact'
<internal:/home/epaew/.rbenv/versions/3.0.1/lib/ruby/3.0.0/rubygems/core_ext/kernel_require.rb>:85:in `require': cannot load such file -- lib/deep_compact (LoadError)
from <internal:/home/epaew/.rbenv/versions/3.0.1/lib/ruby/3.0.0/rubygems/core_ext/kernel_require.rb>:85:in `require'
from (irb):1:in `<main>'
from /home/epaew/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/irb-1.3.5/exe/irb:11:in `<top (required)>'
from /home/epaew/.rbenv/versions/3.0.1/bin/irb:23:in `load'
from /home/epaew/.rbenv/versions/3.0.1/bin/irb:23:in `<main>'
irb(main):002:0>
おそらくですが、
-
::DeepCompact::Array
の構文解析中に::DeepCompact::Hash
の構文解析が必要になり、
その中でまた::DeepCompact::Array
の構文解析が必要になるため、LoadError
が発生する[1]
という状況のようです。
まとめ
- Refinements 難しい
- Ruby の Refinements を使って「1つのモジュールで 2つのクラスを再定義する」実装と、
「2つのモジュールでそれぞれ 1つのクラスを再定義する」実装では、クラスが再定義される scope に差がある- もしかしたら、「同一モジュールであれば
refine Array
内で定義したメソッドがrefine Hash
内から参照できる」この今の挙動こそがバグだったりするのかもしれない
- もしかしたら、「同一モジュールであれば
- Refinements のためのモジュール間で相互参照が発生すると、LoadError が発生する
- Ruby の Refinements を使って「1つのモジュールで 2つのクラスを再定義する」実装と、
おまけ(追記)
その1
lib/deep_compact.rb
lib/deep_compact/array.rb
module DeepCompact
module Array
module Mixin
def deep_compact
compact.map do |value|
value.respond_to?(:deep_compact) ? value.deep_compact : value
end
end
end
refine ::Array do
include ::DeepCompact::Array::Mixin
end
end
end
lib/deep_compact/hash.rb
module DeepCompact
module Hash
module Mixin
def deep_compact
compact.transform_values do |value|
value.respond_to?(:deep_compact) ? value.deep_compact : value
end
end
end
refine ::Hash do
include ::DeepCompact::Hash::Mixin
end
end
end
lib/deep_compact.rb
require_relative 'deep_compact/array'
require_relative 'deep_compact/hash'
module DeepCompact
refine ::Array do
include ::DeepCompact::Array::Mixin
end
refine ::Hash do
include ::DeepCompact::Hash::Mixin
end
end
-
Array#deep_compact
の再定義は、DeepCompact::Array::Mixin#deep_compact
のスコープからは見えない-
[nil, [nil, :a]].deep_compact
ですら再帰処理ができない
-
その2
lib/deep_compact.rb
lib/deep_compact/array.rb
module DeepCompact
module Array
module Mixin
using ::DeepCompact
def deep_compact
compact.map do |value|
value.respond_to?(:deep_compact) ? value.deep_compact : value
end
end
end
refine ::Array do
include ::DeepCompact::Array::Mixin
end
end
end
lib/deep_compact/hash.rb
module DeepCompact
module Hash
module Mixin
using ::DeepCompact
def deep_compact
compact.transform_values do |value|
value.respond_to?(:deep_compact) ? value.deep_compact : value
end
end
end
refine ::Hash do
include ::DeepCompact::Hash::Mixin
end
end
end
lib/deep_compact.rb
require_relative 'deep_compact/array'
require_relative 'deep_compact/hash'
module DeepCompact
refine ::Array do
include ::DeepCompact::Array::Mixin
end
refine ::Hash do
include ::DeepCompact::Hash::Mixin
end
end
-
DeepCompact::Array::Mixin#using
を宣言した時点ではmodule DeepCompact
のrefine
宣言の解析が完了していないため、何も再定義されない-
[nil, [nil, :a]].deep_compact
ですら再帰処理ができない
-
参考
- https://docs.ruby-lang.org/ja/latest/method/Module/i/refine.html
- https://docs.ruby-lang.org/en/master/doc/syntax/refinements_rdoc.html
-
一方的な参照のみであればエラーは出ませんが、相互に参照し合うとエラーになります ↩︎
Discussion