Refinements のスコープについて勉強した内容を紹介します
RubyHiroba 2014 にて、このネタの LT をやりましたが、いまひとつまとまっていなかったので、まとめ直しました
朴訥なモンキーパッチ
まず、Fixnum クラスにこういう変更を適用したいという事にします
gem 'test-unit', '3.0.9'
require 'test-unit'
class Fixnum
def to_hoge
:hoge
end
remove_method :succ
def succ
:overridden
end
end
class MonkeyTest < Test::Unit::TestCase
sub_test_case '通常のメソッド呼び出しをすると' do
test '上書きされた succ を呼び出せる' do
assert { [:overridden, :overridden, :overridden] == (1..3).map{ |n| n.succ } }
end
test '追加された to_hoge を呼び出せる' do
assert { [:hoge, :hoge, :hoge] == (1..3).map{ |n| n.to_hoge } }
end
end
sub_test_case 'eval で呼ぶと' do
test '上書きされた succ を呼び出せる' do
assert { :overridden == eval('1.succ')}
end
test '追加された to_hoge を呼び出せる' do
assert { :hoge == eval('1.to_hoge') }
end
end
sub_test_case '&Symbol 記法を使った場合は' do
test '上書きされた succ を呼び出せる' do
assert { [:overridden, :overridden, :overridden] == (1..3).map(&:succ) }
end
test '追加された to_hoge を呼び出せる' do
assert { [:hoge, :hoge, :hoge] == (1..3).map(&:to_hoge) }
end
end
sub_test_case 'lambda に閉じ込めると' do
test '上書きされた succ を呼び出せる' do
succ = ->(n){ n.succ }
assert { [:overridden, :overridden, :overridden] == (1..3).map(&succ) }
end
test '追加された to_hoge を呼び出せる' do
to_hoge = ->(n){ n.to_hoge }
assert { [:hoge, :hoge, :hoge] == (1..3).map(&to_hoge) }
end
end
sub_test_case 'メソッドの存在確認' do
test '「succ メソッドってある?」「元々あったし、当然ある」' do
assert { 1.respond_to?(:succ) }
end
test 'メソッド一覧に succ は当然ある' do
assert { 1.methods.include?(:succ) }
end
test '「to_hoge メソッドってある?」「追加されてるので、ある」' do
assert { 1.respond_to?(:to_hoge) }
end
test 'メソッド一覧に to_hoge がある。追加されてるので' do
assert { 1.methods.include?(:to_hoge) }
end
end
sub_test_case 'send で呼び出すと' do
test '上書きされた succ を呼び出せる' do
assert { :overridden == 1.send(:succ) }
end
test '追加された to_hoge を呼び出せる' do
assert { :hoge == 1.send(:to_hoge) }
end
end
sub_test_case 'Symbol#to_proc で Proc オブジェクトとして取り出すと' do
test '上書きされた succ を呼び出せる' do
assert { :overridden == :succ.to_proc[1] }
end
test '追加された to_hoge を呼び出せる' do
assert { :hoge == :to_hoge.to_proc[1] }
end
end
sub_test_case 'Object#method で Method オブジェクトとして取り出すと' do
test '上書きされた succ を呼び出せる' do
assert { :overridden == 1.method(:succ)[] }
end
test '追加された to_hoge を呼び出せる' do
assert { :hoge == 1.method(:to_hoge)[] }
end
end
end
大体、取り返しが付かないレベルで世界が変わってしまっているのが分かります
Refinements を使ったパッチ
上記の様なモンキーパッチの影響を最小限にとどめる為の機能として Ruby 2.0 から試験導入されて 2.1 から正式に動いている Refinements という仕組みがあります
この仕組みを使って、先ほどモンキーパッチで実装した例を書き換えてみます
gem 'test-unit', '3.0.9'
require 'test-unit'
module Hoge
refine Fixnum do
def to_hoge
:hoge
end
def succ
:refined
end
end
end
class RefineTest < Test::Unit::TestCase
using Hoge
sub_test_case '通常のメソッド呼び出しをすると' do
test '上書きされた succ を呼び出せる' do
assert { [:refined, :refined, :refined] == (1..3).map{ |n| n.succ } }
end
test '追加された to_hoge を呼び出せる' do
assert { [:hoge, :hoge, :hoge] == (1..3).map{ |n| n.to_hoge } }
end
end
sub_test_case 'eval での呼び出すと' do
test '上書きされた succ を呼び出せる' do
assert { :refined == eval('1.succ')}
end
test '追加された to_hoge を呼び出せる' do
assert { :hoge == eval('1.to_hoge') }
end
end
sub_test_case '&Symbol 記法を使った場合は' do
test '上書きされた succ を呼び出せず、上書き前の succ が呼び出される' do
assert { [2, 3, 4] == (1..3).map(&:succ) }
end
test '追加された to_hoge が見つからない' do
assert_raise(NoMethodError) { (1..3).map(&:to_hoge) }
end
end
sub_test_case 'lambda に閉じ込めると' do
test '上書きされた succ を呼び出せる' do
succ = ->(n){ n.succ }
assert { [:refined, :refined, :refined] == (1..3).map(&succ) }
end
test '追加された to_hoge を呼び出せる' do
to_hoge = ->(n){ n.to_hoge }
assert { [:hoge, :hoge, :hoge] == (1..3).map(&to_hoge) }
end
end
sub_test_case 'send で呼び出すと' do
test '上書きされた succ を呼び出せず、上書き前の succ が呼び出される' do
assert { 2 == 1.send(:succ) }
end
test '追加された to_hoge が見つからない' do
assert_raise(NoMethodError) { 1.send(:to_hoge) }
end
end
sub_test_case 'Symbol#to_proc で Proc オブジェクトとして取り出すと' do
test '上書きされた succ を呼び出せず、上書き前の succ が呼び出される' do
assert { 2 == :succ.to_proc[1] }
end
test '追加された to_hoge が見つからない' do
assert_raise(NoMethodError) { :to_hoge.to_proc[1] }
end
end
sub_test_case 'Object#method で Method オブジェクトとして取り出すと' do
test '上書きされた succ を呼び出せず、上書き前の succ が呼び出される' do
assert { 2 == 1.method(:succ)[] }
end
test '追加された to_hoge が見つからない' do
assert_raise(NameError) { 1.method(:to_hoge)[] }
end
end
sub_test_case 'メソッドの存在確認' do
test '「succ メソッドってある?」「当然ある」' do
assert { 1.respond_to?(:succ) }
end
test 'メソッド一覧に succ はある' do
assert { 1.methods.include?(:succ) }
end
test '「to_hoge メソッドってある?」「そんなものはない」' do
assert { not 1.respond_to?(:to_hoge) }
end
test 'メソッド一覧に to_hoge はない' do
assert { not 1.methods.include?(:to_hoge) }
end
end
end
通常のメソッド呼び出しであれば、想定通りですが、いくつかの呼び出し方法では単なるモンキーパッチとは異なる結果となりました
Monkey と Refine の違い
呼び方 | Monkey | Refine |
---|---|---|
通常 | ○ | ○ |
&Symbol 記法 | ○ | × |
eval | ○ | ○ |
lambda 化 | ○ | ○ |
Object#respond_to? | ○ | × |
Object#send | ○ | × |
Symbol#to_proc | ○ | × |
Object#method | ○ | × |
Object#methods | ○ | × |
この結果を見ると、別のスコープで定義されているメソッドからは、Refine された変更は実行できていないように見えます
と、言いたいところですが、Kernel.#eval だけは違うみたいです
Kernel.#eval
文字列 expr を Ruby プログラムとして評価してその結果を返します。第2引数に Binding オブジェクトを与えた場合、 そのオブジェクトを生成したコンテキストで文字列を評価します。
module function Kernel.#eval
とあるので、eval は Binding オブジェクトを与えなければ、eval が呼ばれたコンテキストで文字列をコードとして評価すると考えられます。先ほどの試行で Refine された変更が実行できたのはそういうことしょう。
であれば、using されたコンテキストを取り出して、別のスコープで eval に与えれば、Refine された変更を実行できそうです。
gem 'test-unit', '3.0.9'
require 'test-unit'
module Hoge
refine Fixnum do
def to_hoge
:hoge
end
def succ
:refined
end
end
end
module Fuga
using Hoge
def self._binding
binding
end
end
class FugaTest < Test::Unit::TestCase
sub_test_case 'class_eval で持ってきた binding を使って eval すると' do
test '上書きされた succ を呼び出せず、上書き前の succ が呼び出される' do
assert { 2 == Fuga.class_eval{ binding }.eval('1.succ') }
end
test '追加された to_hoge が見つからない' do
assert_raise(NoMethodError) { Fuga.class_eval{ binding }.eval('1.to_hoge') }
end
end
sub_test_case 'クラスメソッドで持ってきた binding を使って eval すると' do
test '上書きされた succ を呼び出せる' do
assert { :refined == Fuga._binding.eval('1.succ') }
end
test '追加された to_hoge を呼び出せる' do
assert { :hoge == Fuga._binding.eval('1.to_hoge') }
end
end
end
というわけで
- class_eval で得た Binding オブジェクトを使って eval しても Refine された変更は呼び出せず
- using が使われているスコープの Binding オブジェクトを使って eval すれば、Refine された変更が呼び出せる
ということが確認できました
レキシカルスコープ
いろいろ検討した結果、 多くのプログラミング言語がダイナミックスコープをあきらめたのと同様の理由で 「置き換え」はレキシカルになるべきだとの結論を出しました。
Matzにっき(2010-11-13)
今回調べた事が、このダイナミックかレキシカルかという話なんですかね。多分そうです
まとめ
「置かれた場所で咲きなさい」っていう言葉があったのを思い出しました。スコープの話だったんですね
12 月 18 日
今日は誕生日なので4本の Advent Calendar を書きました。よろしければ、このエントリの他に下記のエントリもご覧ください
誕生日ですが「ウィッシュリストに入れてるからには読めよ」などと言いながら難しい本を送りつけるなどの行為は何卒