Refinementについてグダグダ

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

(2014-10-24: 2.1.2の内容に合わせました)

なんだか思いの外無駄に長くなって、アドベントカレンダーとしては失格な感じになってしまいました。非常に申し訳ない……。
はじめまして、ユスと申します。

はじめにぶっちゃけると ここ の劣化版なので、こっちを読むほうが早いです。あ、石はやめて石は!

以下の内容はバージョン2.1.2をもとに書いてます。

Refinmentとは

Rubyにおけるオープンクラスを利用した、既存のクラスやメソッドに対する、動的な挙動の変更や追加はかなり強力な特徴の一つとなっていますが、これまで主に以下の様な問題を抱えていました。

  • プログラム全体に影響を与えてしまう
  • 変更が見えにくい

前者は、変更自体が全てのクラスに対して適用されること、後者は例えば同じ名前のメソッドを重複して変更した場合に混乱が起きることを考えてもらえればわかりやすいと思います。

これらの弊害に対しては、これまでも、モンキーパッチをモジュールとして分割してincludeするなどして、明示的にするなどの対処法はとられてきましたが、Refinementはより厳密な形で局所的なものにとどめるものになっています。雰囲気をつかむために例を見てみましょう。

test.rb
module SomeExtensions
  refine String do
    def to_camelcase
      split('_').map{|s| s.capitalize}.join('')
    end
  end
end

class RefineTest
  using SomeExtensions

  def initialize
    p 'refinement_is_good'.to_camelcase
  end
end

RefineTest.new                       # => 'RefinementIsGood'
p 'refinement_is_good'.to_camelcase  # =>  NoMethodError

SomeExtensionsモジュールでは、Stringクラスに対するto_calmelcaseメソッドの追加をrefineキーワードで行なっています。この実装は、SomeExtensionsモジュールをusingキーワードでとりこんだRefineTestクラスでは適用されるので、利用できま(RefineTest.newの行)。しかし、それ以外の部分でStringクラスのインスタンスに対しては、利用できないことがわかります(最終行)。

挙動について

概要は上記のとおりですが、以降は多少詳し目の仕様を、主に適用できる範囲(スコープ)と対象を中心に見ていきます。

スコープ

Refinementはかなり厳密なレキシカルスコープで動きます。つまり、usingしたスコープと違うスコープに対しては、適用されません。また、外側よりは内部の、前のよりは後の方が優先度が高く適用されます。

もともとRubyでは「外側」のスコープにある変数は参照できないので、順当な仕様と言えると思います。

scope.rb
module AnExtensions
  refine String do 
    def say_something
      p self + ' foo'
    end 
  end
end

module AnotherExtensions
  refine String do 
    def say_something
      p self + ' bar'
    end 
  end
end

class RefineTest
  using AnExtensions
  'outer1'.say_something            # => 'outer1 foo'

  class Inner
    using AnotherExtensions
    'inner1'.say_something          # => 'inner1 bar'
  end

  'outer2'.say_something            # => 'outer2 foo'

  using AnotherExtensions
  'outer3'.say_something            # => 'outer3 bar'
end

# 拡張クラス内では有効ではない
class RefineTestEx < RefineTest
  'test'.say_something              # => NoMethodError
end

モジュールには適用できない

モジュールのメソッドにはRefinementを適用できません。元記事をパクります。

module.rb
module EnumerableExt
  # エラー!
  refine Enumerable do
  end
end

特異メソッドには適用されない

refineのブロック内での特異メソッドに対してはRefinementは適用されません。これも元記事をパクります。

eigen.rb
module FixnumExt
  # 無意味
  refine Fixnum do
    def self.thing
      puts 'Wrong Fix'
    end
  end

  # こっちを使おう
  refine Fixnum.singleton_class do
    def thing
      puts 'Correct Fix'
    end
  end
end

class A
  using FixnumExt
  def w()
    Fixnum.thing
  end
end

A.new.w              # => Correct Fix

SelectorNamespcaceである

これに関しては戸惑う部分があるかもしれない内容です。まずは例を示します。

local_rebinding.rb
class A
  def say_something
    puts 'A'
  end

  def introduce
    print 'This is '
    say_something
  end
end

instance = A.new
instance.say_something   # => 'A'
instance.introduce       # => 'This is A'

module SomeExtension
  refine A do
    def say_something
      puts 'APlus'
    end
  end
end

using SomeExtension
instance.say_something   # => 'APlus'
instance.introduce       # => 'This is A'   

最後のintroduceに注目。intstanceのintroduceが呼ぶのは元のAクラスのsay_somethingメソッドですので、'This is APlus'ではなく'This is A'の方が画面に出力されます。これは、Refinementがレキシカルスコープで動くことの証拠です。
Aのintroduceメソッドは、それとは違うスコープで適用されたRefinement(この場合はグローバルでのusing SomeExtension)の影響を受けません。このような挙動をSelectorNamespcace と呼ぶそうです(参照)。

逆に、local rebindingは、Refinementの元ネタになったClassBoxesという機能(論文がここ にあります)の特徴で、グローバルスコープで適用されるため、上記の例であればSomeExtensionをusingした場合、introduceで'This is APlus'と表示されます。

元メソッドの挙動を変えないという意味で、意図しない挙動を防ぐことにはなるかもしれないですが、逆に困る場面もあります。
例えば、eachメソッドなどの挙動を、これまで他のクラスのメソッド内で利用されているものも含めて、グローバル的に変更したい場合、refineでは不可能です。GUIならば、他の場所で用いられているボタンの挙動を一括して変更することなどが難しいかもしれません。

実行速度

注意:2.0.0-preview2時点での内容なので間違ってるかもしれません
結論を言うと、ちょっとだけincludeより遅い という程度のようです。詳しくは、ここのBenchmarking Refinementsを見てください(たらい回し)。

まとめ

不勉強ゆえ知らなかったのですが、Refinement自体は、Matzさんによって2006年頃から既に導入が検討されていたっぽいです。(ちなみに、元記事では「SelectorNamespceよりもClassBoxの方がよいかも」とおっしゃってますが、結果的に前者の側に倒しす形で落ち着いたように見えます。私が誤解している可能性が多分にありますががが)

Refinementは制約も大きめですが、モンキーパッチを適用範囲を狭めることで、より見通しのきくものにしてくれると感じます。
いわば、オレオレモンキーパッチ。これまで以上に色々遊べそうです。

  • この記事は以下の記事からリンクされています
  • Ruby メモからリンク