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

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.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 on RailsによるWEBシステム開発、Android/iPhoneアプリ開発、電子書籍配信のことならお任せください この記事を書いた人と働こう! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833 コボラー、ITコンサル、ローカライズ業界を経てなぜかWeb開発者志願。 これまでにRuby on Rails チュートリアルの大半、Railsガイドのほぼすべてを翻訳。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

BPSアドベントカレンダー

人気の記事