Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

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

更新情報

  • 2016/12/06: 初版公開
  • 2023/01/05: 更新

こんにちは、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モジュールの各メソッドは、プライベートメソッドでもあり、モジュールの特異メソッドでもあります。こうすることで、includeselfextendを使わずに少ない記法でプライベート兼特異メソッドを書くことができるようです。

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.hiMyClass::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
```

Ruby: 一回だけ実行されるブロックより

こちらは普通にモジュールでmodule_functionを使っています。これをクラスでくるめば本記事のコードと同じ構造になりますね。

Active Supportのモジュールがクラスでくるまれていたのでつい難しく考えてしまいましたが、最初からそう考えればよかったなと思います。

参考

関連記事

[Ruby]クロージャーを使ってブロックを1回だけ実行する


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。