Refinement関係の小技とできない事をまとめてみた

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

Refinementを実用的に使うために、色々と調査してみて分かったことがいくつかあるのでまとめておく。

できる事

Module#using

まず、Ruby2.1からModule#usingが使えるようになっている。
これはとても素晴らしい。この調子でもうちょっと自由度が上がっていってくれると最高。

Refinementでクラスメソッドを定義する方法

これは駄目な例

module BadClassMethodRefine
  refine String do
    def self.hoge
      p "hoge"
    end
  end
end

using BadClassMethodRefine

String.hoge # NoMethodError

こうすると定義できる

module ClassMethodRefine
  refine String.singleton_class do
    def hoge
      p "hoge"
    end
  end
end

using ClassMethodRefine

String.hoge

refineブロックの中でp selfとすると分かるのだが、この中はクラスのコンテキストではなく、Refinement用の特殊なモジュールのスコープとして評価されている。
p selfすると、#<refinement:#<Class:String>@ClassMethodRefine>って感じの出力が出てくる
なので、ここでdef self.hogeとやっても意味が無い。
直接クラスの特異クラスを引っ張ってきて、特異クラスのインスタンスメソッドとして定義する必要がある。

refineは後からでもできる

module RefineModule
end

RefineModule.module_eval do
  refine Fixnum do
    def |(other)
      [self, other]
    end
  end
end

using RefineModule

1 | 2

refineについては制限がそんなに無くて、後からmodule_evalして定義しても大丈夫。
ただし、Module内に定義されるより前にusingを実行してもメソッドは見つからない。

refineにProcは渡せないが、module_evalはできる

refineにProcを渡そうとすると怒られる。

module ErrorRefineModule
  definition_proc = -> {
    def dynamic_def
      p "dynamic_def"
    end
  }

  refine(String, &definition_proc)
end
refine.rb:43:in `refine': can't pass a Proc as a block to Module#refine (ArgumentError)
        from refine.rb:43:in `<module:ErrorRefineModule>'
        from refine.rb:36:in `<main>'

しかし、refine内でmodule_evalはできる

module DynamicRefineModule
  definition_proc = -> {
    def dynamic_def
      p "dynamic_def"
    end
  }

  refine String do
    module_eval(&definition_proc)
  end
end

instance_evalじゃないので注意。

これを利用して一つgemを作ってみた。

joker1007/refining

クラス定義の中でインラインモジュールとして自クラスに関するRefinementモジュールを定義する時、インデントを一つ減らすだけのgem。
意味あるかなあ……。まあとりあえず自分でこういう書き方を適度に試してみている。

できない事

メソッド内でusingを利用できない

class Module
  def using_in_method(mod)
    using mod
  end
end

module Foo
  using_in_method Hoge
end
refine.rb:13:in `using': Module#using is not permitted in methods (RuntimeError)
        from refine.rb:13:in `using_in_method'
        from refine.rb:41:in `<module:ErrorRefineModule>'
        from refine.rb:40:in `<main>'

module_evalでusingできるが、そのブロックから外では効果が無いし、eval系もかなり制限がある

module RefineModule
  refine String do
    def refined_method
      p "refined_method"
    end
  end
end

module Foo
end

Foo.module_eval do
  using RefineModule

  "foo".refined_method
end

Foo.module_eval do
  "foo".refined_method # こっちでは使えない
end

これはクラス内でusingして、別の場所でオープンクラスした時と同じ挙動。
つまりusingしたスコープ内でのみ効果がある。
他からもってきたprocを無理矢理instance_evalなりmodule_evalしても効果は無い。(残念だ)

ブロックではなくテキストとして引き渡すevalだったら外部から無理やり何か差し込むのは不可能じゃないが、実用性には乏しい。

RSpecとの組み合わせ

RSpecのdescribeは実はExampleGroupを親クラスとした無名クラスの定義と同じことをしている。
つまり、describeブロックの中はクラス定義と同じなので、中でModule#usingが使える。
describeのブロックごとに別々のクラスとして定義されているので、そのスコープから出るとちゃんとRefinementが外れてくれる。

これを利用して、組み込みクラスを局所的に書き換えてテストに活用できる。
私がコントリビュートしているtomykaira/rspec-parameterizedというgemに試験的に実装してみた。

describe "plus" do
  using RSpec::Parameterized::TableSyntax

  with_them do
    it "should do additions" do
      (a + b).should == answer
    end
  end

  where(:a, :b, :answer) do
    1 | 2 | 3  >
    5 | 8 | 13 >
    0 | 0 | 0
  end
end

Groovyっぽくパラメタライズドテストを書けるようにするシンタックスをRefinementで実現している。
(行末に苦肉の策感が残っているが……)

とりあえず全体としてはusingを自分でちゃんと指定できる用途なら、何かしらに使えそうな気がする。
例えば、Rails4.1のconcerningを参考にして、インラインモジュールでrefineを定義しておいて特定のコンテキストの時だけusingして利用すれば、
明示的に利用が宣言されない限りは、他で一切利用できないメソッドが定義できる。
まあ、インラインモジュールを定義しておいて、extendするのと余り変わらない、という気もするのだが……。
現状のRefinementはRSpecと相性が良いので、テストしやすいというメリットぐらいはあるかもしれない。

後はやっぱりConcernモジュールやconcerningの中で使うって方向かなあ。