こんにちは、hachi8833です。先日の記事「[Rails5] Active Support Core ExtensionsのStringクラス(2)html_safe」を書いていて見つけた、module_function
の使い方を別記事にいたしました。
条件
- Rubyバージョン: 3.2.0
- Railsバージョン: 7.0.2のActive Support
モジュールでのメソッド定義とmodule_function
Active SupportのERBクラスを見てみると、Utilモジュールが定義されており、Ruby標準のERBライブラリをオーバーライドします。
ERBクラスはざっくり以下のような構成になっています。
# ERBクラスの構成
class ERB
module Util
def メソッド1 ... end
module_function :メソッド1
def メソッド2 ... end
module_function :メソッド2
...
end
end
上のように、Utilモジュールではmodule_function
メソッドが多用されています。たとえば以下のように、各モジュールのメソッド定義後にmodule_function
が呼び出されています。
# A utility method for escaping HTML without affecting existing escaped entities.
# ...
def html_escape_once(s)
result = ActiveSupport::Multibyte::Unicode.tidy_bytes(s.to_s).gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
s.html_safe? ? result.html_safe : result
end
module_function :html_escape_once
この書き方が気になったので、調べてみました。
Rubyのモジュール関数とは
module_function
と密接に関連するモジュール関数について改めて確認してみました。
モジュール内で定義されたメソッド(本記事でのみ、仮にモジュールメソッドと呼ぶことにします)は、そのモジュール自身の内部や、モジュールをincludeした場所(クラスなど)で呼び出すことができます。
しかしモジュールメソッドは、そのままではモジュールの外部に公開されていない(デフォルトでprivate
)ので、モジュール名.メソッド名
の形で外部から呼び出すことはできません。
モジュールメソッドを外部に公開するにはmodule_function
が必要です。公開したいモジュールメソッドはシンボルで与えます。
引数が与えられた時には、引数で指定されたメソッドをモジュール関数にします。引数なしのときは今後このモジュール定義文内で新しく定義されるメソッドをすべてモジュール関数にします。
モジュール関数とは、プライベートメソッドであると同時にモジュールの特異メソッドでもあるようなメソッドです。例えば Math モジュールのメソッドはすべてモジュール関数です。
Ruby 3.2.0 リファレンスマニュアルより
つまりERBクラスのUtilモジュールの各メソッドは、プライベートメソッドでもあり、モジュールの特異メソッドでもあります。こうすることで、include
やself
やextend
を使わずに少ない記法でプライベート兼特異メソッドを書くことができるようです。
module_function
の動作を確認する
IRBで試してみました(プロンプトはカスタマイズしてあります)。
# module_functionを使った場合
class MyClass
module Util
def hello
"hello"
end
module_function :hello
end
def hi
Util.hello
end
end
スクリーンショットでは
def hello()
のようにかっこが残っていますが、引数なしの場合かっこは使わないのが普通です。
確かに、特異メソッドMyClass::Util.hello
とインスタンスメソッドhi
(内部でUtil.hello
を呼んでいる)だけが有効になっています。
module_function :hello
がない場合
module_function :hello
行がないと、a.hi
も動かなくなります。
# module_functionがない場合
class MyClass
module Util
def hello
"hello"
end
end
def hi
Util.hello
end
end
ただし、クラス定義やモジュール定義の後であれば、外で明示的にinclude MyClass::Util
するとa.hi
やMyClass::Util.hello
が動くようになります。
名前空間が異なるので、Util.hello
は動きませんし、include Util
もできません。
追伸: module_function
なしでプライベート兼特異メソッドを書くとどうなるか
プライベート兼特異メソッドをmodule_function
なしで書くにはいくつかの方法があるようですが、とりあえず以下が考えられます。
- モジュール内のメソッド定義を
self
で特異メソッドにする
# self.hello特異メソッド
class MyClass
module Util
def self.hello
"hello"
end
end
def hi
Util.hello
end
end
期待どおりの動作です。しかし多くの人が「定義のたびにself
つけるのうざい」と感じているようです。
なお、モジュールがクラスの外にある場合でも同様に動作します。当初MyClassでexpand Util
する必要があるかと思っていましたが不要でした。
# UtilがMyClassの外にある場合
module Util
def self.hello
puts "hello"
end
end
class MyClass
def hi
Util.hello
end
end
MyClass.new.hi
というショートハンドをbabaさんから教わりました。ありがとうございます!
追伸
先日のTechRacho記事「[Ruby]クロージャーを使ってブロックを1回だけ実行する」で引用した高林哲さんのコードでもmodule_function
が使われていますね。
```ruby
module Kernel
@@once_executed = Hash.new
def once
location = caller.first
# メッセージ表示だけなら値の保存は不要だが、せっかくなので一般化
value = if @@once_executed.has_key?(location)
@@once_executed[location]
else
@@once_executed[location] = yield
end
return value
end
module_function :once
end
```
こちらは普通にモジュールでmodule_function
を使っています。これをクラスでくるめば本記事のコードと同じ構造になりますね。
Active Supportのモジュールがクラスでくるまれていたのでつい難しく考えてしまいましたが、最初からそう考えればよかったなと思います。
更新情報