こんにちは、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のモジュールがクラスでくるまれていたのでつい難しく考えてしまいましたが、最初からそう考えればよかったなと思います。
 
       
                      




更新情報