[Ruby] module_functionでモジュールの特異メソッドを簡潔に書く

こんにちは、hachi8833です。先日の記事「[Rails5] Active Support Core ExtensionsのStringクラス(2)html_safe」を書いていて見つけた、module_functionの使い方を別記事にいたしました。 条件 Rubyバージョン: 2.3.3 Railsバージョン: 5-0-stable の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が必要です。公開したいモジュールメソッドはシンボルで与えます。 引数が与えられた時には、 引数で指定されたメソッドをモジュール関数にします。 引数なしのときは今後このモジュール定義文内で 新しく定義されるメソッドをすべてモジュール関数にします。 モジュール関数とは、プライベートメソッドであると同時に モジュールの特異メソッドでもあるようなメソッドです。 Ruby 2.3.0 リファレンスマニュアルより つまりERBクラスのUtilモジュールの各メソッドは、プライベートメソッドでもあり、モジュールの特異メソッドでもあります。こうすることで、includeやselfやextendを使わずに少ない記法でプライベート兼特異メソッドを書くことができるようです。 参考: Rubyのモジュールメソッドの作り方 module_functionの動作を確認する pryで試してみました。 # 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 … Continue reading [Ruby] module_functionでモジュールの特異メソッドを簡潔に書く