Ruby: Mixinモジュールに private メソッドを作る
TL;DR
- Mixin用のモジュールで、それをインクルードするクラスからも見えないような private スコープが欲しい。
- Ruby2.4以降なら、自分の中で自分自身を refine する事でそれっぽい事はできる。
- 通常の private とは違うしイマイチな点もある。
サブクラスからも見えない private スコープ
Mixin用にモジュールを作った時、「それをインクルードするクラスからも見えない private スコープ」 が欲しくなる事ってありませんか?
Rubyの private はそのクラス/モジュールを継承するクラスからも見えてしまうので、 Mixin用モジュールの中で private メソッドを定義しても、それをインクルードするクラスから呼べてしまいます。 特に、例えば Rails の Helper のように色んなところでインクルードされるモジュールでは、 インクルードするクラスから見えない private スコープが欲しくなる時が自分はあります。
module SomeUtil
def do_something
# ...
end
private
# private method を定義しても...
def secret
"some secret"
end
end
class Someone
include SomeUtil
# include したクラスからは見えちゃう
def leak
"I know #{secret}"
end
end
p Someone.new.leak
#=> "I know some secret"
解決方法をちょっとググってみると、モジュール内に別のクラスを定義してその中にメソッドを置く方法がありました (例)。
実際に private になるわけではありませんが、ネームスペースの汚染を防ぐには有効そうです。
とはいえクラスが変わるとself
も変わってしまって何だかなーという気がするし、
いちいちクラスを定義するのも面倒です。
ActiveSupportが使っていた方法
そんな中、先日社内勉強会でActiveSupportのソースを読んでいた時に面白いコードを見つけました。
Array#sum
を拡張する以下のようなコードです (source)。
module Enumerable
# ...
# Using Refinements here in order not to expose our internal method
using Module.new {
refine Array do
alias :orig_sum :sum
end
}
class Array
def sum(init = nil, &block) #:nodoc:
if init.is_a?(Numeric) || first.is_a?(Numeric)
init ||= 0
orig_sum(init, &block)
else
super
end
end
end
end
ここではsum
メソッドを再定義する前に、
元々あるArray#sum
にもアクセスできるようにorig_sum
という alias をつくっているのですが、
興味深いのはその処理がusing
とrefine
によるブロックの中で行われている点です。
using
とrefine
RefinementsはRuby2.0から導入された機能で、 限定した範囲でのみオープンクラスによるクラス拡張を有効にする事ができます。
module StringExtensions
refine String do
def loudly(volume = 3)
"#{self}#{'!' * volume}"
end
end
end
module Hello
using StringExtensions
def self.world
"hello, world".loudly
end
end
p Hello.world # => "hello, world!!!"
p "good bye".loudly # => NoMethodError
しかし先程ActiveSupportが使っていた Refinements は、このような例とは違いました。
その場で無名モジュールを作り、その中でrefine
を使ってArray
に alias を追加し、即座にそれをusing
しています。
このコードはEnumerable
モジュール内に書かれているので、こうするとEnumerable
内でのみorig_sum
メソッドが使えるようになります。
つまり、orig_sum
という alias を外部に公開する事なく使えるわけです。面白いですね。
ところでこの方法って、通常のモジュールでも使えないでしょうか?
つまり、自分自身の中でのみ自身をrefine
& using
してメソッドを定義すれば、
自分自身の中でのみ使用可能なメソッドを定義できるのではないでしょうか
(言うなれば self-refinements?)。
Self Refinements による擬似 private
実は、 2.3 までの Ruby ではモジュールでこの方法を使う事は不可能でした。
refine
はクラスにしか使えず、モジュールを渡すとエラーになっていたからです。
しかし 2.4 からはモジュールにも使えるようになったため (release note)、
それが可能になりました。実際にやってみます。
module Greeter
using Module.new {
refine Greeter do
def world
"world"
end
end
}
def hello
"hello, #{world}"
end
end
class Stuff
include Greeter
def greet
hello
end
def greet2
world
end
end
p Stuff.new.greet # => "hello, world"
p Stuff.new.greet2 # => undefined local variable or method 'world'
動きました。 もう少し簡単に書けるようにするとこんな感じでしょうか。
class Module
# Add `privates` method to Module class.
def privates(&block)
target = self
Module.new do
refine target do
class_eval &block
end
end
end
end
module Greeter
# Define private-like methods using Refinements feature.
using privates {
def world
"world"
end
}
def hello
"hello, #{world}"
end
end
class Stuff
include Greeter
def greet
hello
end
def greet2
world
end
end
p Stuff.new.greet # => "hello, world"
p Stuff.new.greet2 # => undefined local variable or method 'world'
using
をメソッド内で使う事は禁止されているので、refine
の部分だけメソッド化しています。
これでもう少し簡単に書けるようになりました。
この方法なら、サブクラスからも見えない private スコープを手軽に実現する事ができます。
問題いろいろ
が、この方法にはいくつかイマイチな点があります。
定義ブロックにdo ... end
を使えない
以下のようにprivates
に渡すブロックをdo end
で記述すると、
そのブロックはusing
への引数だと解釈されてしまうため、
代わりにブレース ({}
) を使う必要があります。
# ArgumentError: wrong number of arguments
using privates do
def world
"world"
end
end
# => `using(privates) do; end`
# OK
using privates {} # => using(privates {})
# OK
using (privates do; end)
複数行になるブロックにはdo end
を使う方が一般的だと思うので、
ブレースで書かないといけないのはちょっと気持ち悪いです。
using privates
の定義場所に気をつけないといけない
using
文は、それによって拡張されるメソッドを使用している箇所よりも前に記述する必要があるようです。
例えば以下のように順番を変えてhello
を実行すると、「world
は未定義です」というエラーになってしまいます。
module Greeter
def hello
"hello, #{world}" # Error
end
using privates {
def world
"world"
end
}
end
定義したメソッドをsend
で呼べない
これは当然ですが、
Refinements が効いていないモジュールの外からはrefine
ブロックで定義されたメソッドは全く見えないので、
通常の private とは違いsend
を使っても呼び出す事ができません。
逆に言えば、他の言語にあるような完全な private を実現できる、とも言えるかもしれませんが。
まとめ
自身の Refinements を自身内でのみ使う事で、外部からは見えないメソッドを定義できました。 この「外部」にはサブクラスも含まれるので、 Mixin用モジュール内でのみ使える private-like なメソッドを定義できます。 また通常のクラスでも、サブクラスに公開したくないメソッドを定義するのに使えます。 ただし、記述方法は微妙と言わざるを得ません。