Ruby の refinement は,クラスやモジュールの改変(メソッドの追加・修正など)を限定されたスコープ内でのみ有効にする仕組みだ。
これを使って Array クラスにクラスメソッドを追加しようとしたが,その方法に迷い,最終的に解決したので記事にした。
refinement とは
refinement をご存知の方は飛ばして次節へどうぞ。
たとえば,Array クラスに,「要素(数値)の平均値を返すメソッド average」を追加したいとする。
refinement を使わない場合,
class Array
def average
sum.fdiv(length)
end
end
となるだろう。
しかし組込みクラスの変更は思わぬところに影響を与えうるので,たいへん危なっかしい1。
Array#average を MyAwesomeClass2 の中だけで使いたいなら,refinement を使って以下のように書ける。
module AddAverageToArray # ← モジュール名は何でもいい
refine Array do
def average
sum.fdiv(length)
end
end
end
class MyAwesomeClass
# ここでは Array#average は使えない
using AddAverageToArray
# ここ以降,Array#average が使える
# ただし,このクラス定義式の中だけ
end
試行錯誤
この節では私が試行錯誤した過程を書く。やり方だけ知りたい方は次節へどうぞ。
以下のような馬鹿馬鹿しいクラスメソッドを,refinement で実現したいとしよう。
class Array
def self.speak
puts "あれぇ"
end
end
def self.speak で
最初に試したのは以下のようなやり方。
module AddSpeakToArray
refine Array do
def self.speak
puts "あれぇ"
end
end
end
クラス定義式中でクラスメソッドを定義するのに,def self.speak みたいに書くのはごく普通だよね。
ところが使おうとすると
using AddSpeakToArray
Array.speak
# => undefined method 'speak' for class Array (NoMethodError)
なんと,Array.speak が定義されていない。
どゆこと?
もしかして,refine Array のブロック内の self は Array じゃないってこと?
確かめよう:
module AddSpeakToArray
refine Array do
p self
# => #<refinement:Array@AddSpeakToArray>
p self.class
# => Refinement
end
end
そうだったのか。
Array じゃなくて,Refinement というクラスのオブジェクトだった。
このクラスを直接どうこうした経験がなく,存在すら知らんかった。
def Array.speak で
クラスメソッドを定義する方法はいくつもある。
refine Array のブロック内で self が Array ではない,ということなら,self でなく Array と書けばいいじゃん。
module AddSpeakToArray
refine Array do
def Array.speak
puts "あれぇ"
end
end
end
実際,これで使えた:
using AddSpeakToArray
Array.speak
# => あれぇ
やったー,解決!
……とはならんのよ。
念のため有効範囲が限定されていることを確認しようと,using AddSpeakToArray を消してみた。
それでも Array.speak が使えてしまう!
特異クラスを refine
最後に試したのが,Array の特異クラスを refine する方法。
以下のことを思い出そう。
- Array のクラスメソッドとは,Array の特異メソッドに他ならない。
- オブジェクト
xの特異メソッドは,xの特異クラスのインスタンスメソッドである。 - オブジェクト
xの特異クラスはx.singleton_classで得られる。
これを踏まえれば以下のように書ける。
module AddSpeakToArray
refine Array.singleton_class do
def speak
puts "あれぇ"
end
end
end
これだと,using AddSpeakToArray したときのみ Array#speak が使える。
解決だ。