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を作ってみた。
クラス定義の中でインラインモジュールとして自クラスに関する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
の中で使うって方向かなあ。