Edited at

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

More than 3 years have passed since last update.

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の中で使うって方向かなあ。