名前空間にやさしいRefinementsの使い方
Ruby2.1にはRefinementsが正式に導入された。ネットで検索すると色々出てくるが、特に衝撃的な便利さということもなく、知ってるけど使ったことは無い、みたいな人も多そうである。ぶっちゃけ、どんなときに使うの?って聞かれると答えにくい。
そうだなー。例えば。
前にPDFを生成するプログラムを作ったことがあるが、それはPDFのオブジェクトをRubyのArrayとかHashとかで代替的に使う仕掛けだったのでそれぞれto_pdfというメソッドを追加していた。そうするとそのライブラリを使う人すべてに影響してしまうので、Refinementsで局所的にメソッドを追加するのが良い。まあ当時は無かった。
逆に、自分で何かしら標準クラスの機能を変更したい場合、それを使ってるライブラリなどがあると動かなくなってしまうので、自分のコードに限定するためにRefinementsを使う。ただ、そういう例はあまり見ない。
ということで今回はRefinementsの話。
基本的なところ
モジュール内でrefineして他のクラスにメソッドを生やしておいて、そのモジュールを別のところでusingするとそのメソッドが呼べるようになる。
こんな感じ。
module Ref # Refモジュールで refine Array do # Arrayをrefineして def aaa # aaaを定義すると p "aaa" end end end class Hoge # Hogeクラスで using Ref # Refモジュールをusingして def aaa [].aaa # Arrayのaaaが呼べるようになる end end Hoge.new.aaa #=>"aaa"
レキシカルスコープとダイナミックスコープ
Rubyはほぼダイナミックスコープで動作するので、レキシカルスコープを意識することは少ない。
ダイナミックスコープというのは、まあ、例えばmodule_evalとかinstance_evalとかで別のオブジェクトのスコープで動かすような真似ができる、という部分。レキシカルスコープというのは、コード上の書かれた場所が影響する部分。
どういう時に意識するかというと、このようなコードを書いたとき。
class A Hoge = 1 class B p Hoge #=>1 end end class C Hoge = 2 end class C::D Hoge #=> uninitialized constant C::D::Hoge (NameError) end
定数のスコープはレキシカルに検索されるのでC::Dの中で定数Hogeを参照することができない。あとはローカル変数の定義とか?
Refinementsのusingはレキシカルに作用するのでコードの見た目上「usingを書いたモジュールの中」でしか影響が発生しない。
このあたりの話は検索すると出てくるので気になる人は調べてみよう。
Refinementsの弱点と解決策
ずばり、refineするためにモジュールが必要なところである。これで定数が作られてしまうと外からも見えるようになってしまうので、特にライブラリを作る人にとっては非常に使いづらい。
しかし必要なのはModuleオブジェクトであって名前ではないので、using時に生成してしまえば名前空間が汚染されることは無くなる。
class Hoge using Module.new { refine Array do def aaa p "aaa" end end } def aaa [].aaa end end Hoge.new.aaa #=>"aaa"
これで使いやすくなった。
もういっちょ
ところで、上記の例でArrayにaaaメソッドが生えていて欲しいのはHoge#aaaの中だけだったとしよう。んでもusingするとHogeクラス全体に影響が及ぶ。これに対処するのもやっぱりModuleの動的生成となる。こんな感じ?
Module.new { using Module.new { refine Array do def aaa p "aaa" end end } def self.aaa [].aaa end }.aaa #=>"aaa"
Refinementsの影響を完全に封じ込めることができた。
おしまい
うーん、やっぱりいい用途が思いつかない。でも覚えておけばいずれピンポイントで役に立つときもくるんじゃないかな。
モンキーパッチの影響を局所的に押さえ込むようなものだと思うから、何かしらの問題の対処とかそういうのに使うものなのかもしれない。