Refinements のスコープについて

  • 22
    いいね
  • 1
    コメント
この記事は最終更新日から1年以上が経過しています。

Refinements のスコープについて勉強した内容を紹介します
RubyHiroba 2014 にて、このネタの LT をやりましたが、いまひとつまとまっていなかったので、まとめ直しました

朴訥なモンキーパッチ

まず、Fixnum クラスにこういう変更を適用したいという事にします

monkey_test.rb
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 という仕組みがあります
この仕組みを使って、先ほどモンキーパッチで実装した例を書き換えてみます

refine_test.rb
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 された変更を実行できそうです。

eval.rb
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 を書きました。よろしければ、このエントリの他に下記のエントリもご覧ください

誕生日ですが「ウィッシュリストに入れてるからには読めよ」などと言いながら難しい本を送りつけるなどの行為は何卒

この投稿は Ruby Advent Calendar 201418日目の記事です。