🎃

[Ruby] モジュール関数から、同じモジュールのプライベートメソッドを呼び出す

2022/03/03に公開

モジュールで、「他のインスタンスメソッドbarを内部で呼ぶ、インスタンスメソッドfoo」に対してmodule_function :fooを使って特異メソッドを定義した場合に、あれ、特異メソッドとして使おうとすると他のメソッドbarが内部で呼び出せない...となるケースに対する記事です。

module M
  def foo
    bar # 他のインスタンスメソッドbarを呼び出す
  end

  def bar
    puts "bar!"
  end

  module_function :foo # fooだけ
end

M.foo
# => in `foo': undefined local variable or method `bar' for M:Module (NameError)
用語の整理
module M
  def self.module_singular_method # モジュールの特異メソッド
    puts "特異メソッド"
  end

  def module_instance_method # モジュールのインスタンスメソッド
    puts "インスタンスメソッド"
  end
end

class C
  include M
end

M.module_singular_method #=> "特異メソッド"
C.new.module_instance_method #=> "インスタンスメソッド"

まとめ

モジュールの特異メソッドからは、同じモジュール内に定義したインスタンスメソッドを呼び出せない

モジュールの特異メソッドは、

  1. まずインスタンスメソッドfooを定義する

  2. 適宜、連携する別のインスタンスメソッドbarも定義する

  3. 1のfooに対してmodule_function :fooを使って、fooをモジュール(仮にBaz)の特異メソッドにもする

というステップで定義することも多いと思います。

このとき、fooを特異メソッドとして呼び出す(Baz.foo)と、インスタンスメソッドfoo, barをまず定義したときの認識とは裏腹に、Baz.foobarを呼び出せないのでエラーになってしまいます。

具体的には、以下のようなことをしようとすると、失敗してしまいます。
(Ruby3.0以降を想定して、パターンマッチとエンドレスメソッド定義構文を使っています。)

module Calculator
  private

  def calc(x, y, type) # インスタンスメソッドとして定義して...
    case type
    in :add
      add(x, y)
    in :div
      div(x, y)
    end
  end
  module_function :calc #=> calcはパブリックな特異メソッドにもなった

  # addとdivはプライベートインスタンスメソッドのままでいいつもり
  def add(x, y) = x + y
  def div(x, y) = x / y
end

puts Calculator.calc(3, 4, :add) # モジュールの特異メソッドとしてcalcを実行!
#=> 7:in `calc': undefined method `add' for Calculator:Module (NoMethodError)

一方、以下のように、他のクラスでincludeして、インスタンスメソッドのcalcとしてadddivを呼び出すのは、(想定通り)問題ありません。これはこのままでよいとします。

# - module Calculatorの定義部分省略 -

class Genius
  include Calculator # calc, add, divがプライベートインスタンスメソッドに

  def hyper_calc(x, y, type)
    # calcをインスタンスメソッドとして呼び出し
    puts "#{type} #{x} with #{y} equals #{calc(x, y, type)}."
  end
end

alice = Genius.new
alice.hyper_calc(3, 4, :add)
#=> add 3 with 4 equals 7.

では、当初の

  • calcはモジュールCalculatorのパブリックな特異メソッドにしたい

  • adddivはプライベートインスタンスメソッドにしたい

    • つまり、モジュールCalculatorの(外から呼べる)特異メソッドにはしたくない
    • includeされた先でプライベートインスタンスメソッドになるのは別にいいよ

を満たしたまま、

  • でもでも、calcからadddivを呼び出して利用したい

という場合、どうすればよいでしょうか。

対応方法

結論から先に言うと、特異メソッドから呼び出したいならば、呼び出したい(名前と中身の)メソッドを特異メソッドに用意するしかありません。

手でコピペ&調整で定義するのは冗長かつ危ないので、以下のようにします。

module_function+private_class_methodの方法

今回の場合、adddivに対して、

  • module_functionで特異メソッド&プライベートインスタンスメソッド化して
    (厳密には、同名・同じ機能のメソッドをそれぞれ定義してもらう)

  • private_class_methodで、モジュール外部からの特異メソッド呼び出しを封じる

とするのが、一番素直でしょう。

 module Calculator
   private # module_functionがcalc, add, div全てに設定するので消しても良いが、
           # 将来新たなメソッド定義時の、可視性設定事故防止のために、そのままにする

   def calc(x, y, type)
     case type
     in :add
       add(x, y)
     in :div
       div(x, y)
     end
   end
   module_function :calc

   def add(x, y) = x + y
   def div(x, y) = x / y
+  module_function :add, :div # 特異メソッド/プライベートインスタンスメソッドに
+  private_class_method :add, :div # privateな特異メソッドに
 end

(この例では、privateは機能的にはいらなくなりますが、将来新たなプライベートインスタンスメソッドを定義するときに、『周りのみなさんがプライベートなので自分もそうだと思い込んでprivate設定し忘れていました』という事故を防ぐためにも、そのままで良いと思います。)

(参考)FileUtilsライブラリでの便利メソッド定義の例

Rubyのライブラリでは、例えばFileUtilsで、上記をまとめたprivate_module_functionという特異メソッドを定義してmodule_functionと同じような形で使っていました(なるほど!)。

lib/fileutils.rb
module FileUtils
  # ...
  
  def self.private_module_function(name)
    module_function name
    private_class_method name
  end

  # ...
  
  def remove_trailing_slash(dir)
    dir == '/' ? dir : dir.chomp(?/)
  end
  private_module_function :remove_trailing_slash
  
  # ...

self.fooprivate_class_methodの代わりに、class << selfprivateでもよい

self.fooの形で特異メソッドを定義する場合は、プライベート化をprivate_class_methodで行う必要がありますが、class << selfの形で特異メソッドを定義する場合は、privateでプライベート化することもできます。

module M
  def self.foo
  end
  private_class_method :foo

  class << self
    def bar
    end

    private

    def baz
    end
  end
end

M.private_methods.include?(:foo) #=> true
M.private_methods.include?(:bar) #=> false
M.private_methods.include?(:baz) #=> true

extend selfする方法(*要件未達)

別の方法として、

  • calcをパブリックな、add, divをプライベートなインスタンスメソッドとして定義し、

  • extend selfで、module Calculator自身に、同名・同じ可視性・同機能の特異メソッドを定義する

という方法もあります。

 module Calculator
-  private # <= calcはパブリックな特異メソッドにしたい
-
   def calc(x, y, type)
     case type
     in :add
       add(x, y)
     in :div
       div(x, y)
     end
   end
-  module_function :calc

+  private
+
   def add(x, y) = x + y
   def div(x, y) = x / y
+
+  extend self # Calculator(自分)に、特異メソッドcalc, add, divを定義
 end

ただし、注意点として、

  • calcが、インスタンスメソッドとしてパブリックになる(元の要件と不一致

    • include Calculatorした場合、calcメソッドがパブリックになるということ
  • パッと見、意図が分かりづらい

    • 特に、パブリック・プライベートインスタンスメソッドが混ざっている状態では

というのがあるので、本記事の意図で積極的に使うのは、個人的には微妙だと思います。
(どちらかというと、extend selfを見かけた場合のヒントとして記載しました。)

しくみの確認

詳しくは長くなるので別記事にします<かいてりんくはる>

意識しておきたいこと

本記事と似たようなことで悩んだ場合、強く意識したいことを端的に言えば、クラス定義内であってもモジュール定義内であっても、特異メソッド・インスタンスメソッド定義の仕方とそのレシーバの関係に何も違いはない、ということです。

そして、モジュール定義特有のmodule_functionは、「なんかいい感じのこと」ではなく具体的な同名・同機能のパブリック特異メソッドを定義する」「それと、インスタンスメソッドをプライベートにするという、手動でできることを自動でやってくれるもの、あるいは逆に言えば、手で再現できるもの、というのも意識したほうがよいでしょう。
これは、クラス定義でよく使われるattr_accessor系と同じです。

動きへの理解を深めるために

クラス定義とモジュール定義それぞれでのメソッド定義に違いがないことを利用して、本記事のようなmodule_function絡みのケースで「ん?」となった場合は、module_functionが使えなくなるクラス定義に置き換えてみると、何が抜けているのかがわかりやすいと思います。

つまりは、module_functionがやっていることを自分の手でやってみよう、ということでもあります。

モジュール定義をクラス定義に置き換えてみる

具体的には、以下のようにします:

  • モジュール定義のmoduleclassに置き換える

  • module_functionで定義した特異メソッドfooを、同名で手で明示的に定義(def self.foo)しなおす

    • このとき、メソッド内の他メソッドbarの呼び出しは、全部同名の特異メソッドの呼び出しself.barに置き換える
    • (インスタンス変数...もクラス変数なりクラスインスタンス変数なりに置き換えるが、別問題として、そもそもモジュールのインスタンスメソッド内でインスタンス変数を使うこと自体があまりよろしくない設計(include先のインスタンス変数をいじってしまうため)[1]なのでスルー)
  • module_functionを消す

    • このとき、インスタンスメソッドにprivate設定を(なければ)する(一応)
  • private_class_methodを、手で定義した特異メソッドの近くに移動する(見やすさのため)

具体例として、本記事最初の方で示したNG例・OK例それぞれの置き換えをやってみましょう。

NG例を置き換えてみる

オリジナルでは、calcのみにmodule_functionを適用していたので、

NG例オリジナル
module Calculator
  private

  def calc(x, y, type)
    case type
    in :add
      add(x, y)
    in :div
      div(x, y)
    end
  end
  module_function :calc

  def add(x, y) = x + y
  def div(x, y) = x / y
end

こうします:

NG例クラス定義に置き換え
-module Calculator
+class Calculator # `module`を`class`に
+  def self.calc(x, y, type) # 特異メソッドを定義
+    case type
+    in :add
+      self.add(x, y) # `self.`を追加 => 失敗する呼び出し
+    in :div
+      self.div(x, y) # `self.`を追加 => 失敗する呼び出し
+    end
+  end
+
   private

   def calc(x, y, type)
     case type
     in :add
       add(x, y)
     in :div
       div(x, y)
     end
   end
-  module_function :calc

   def add(x, y) = x + y
   def div(x, y) = x / y
 end

self.add, self.divがないなァ、というのが一目でわかります。

OK例を置き換えてみる

OK例では、calc, add, divmodule_functionを適用していました。
また、self.add, self.divに対しては、private_class_methodが適用されています。
これらを踏まえると、

OK例オリジナル
module Calculator
  private

  def calc(x, y, type)
    case type
    in :add
      add(x, y)
    in :div
      div(x, y)
    end
  end
  module_function :calc

  def add(x, y) = x + y
  def div(x, y) = x / y
  module_function :add, :div
  private_class_method :add, :div
end

こうなります:

OK例クラス定義に置き換え
-module Calculator
+class Calculator # `module`を`class`に
+  def self.calc(x, y, type) # 特異メソッドを定義
+    case type
+    in :add
+      self.add(x, y) # `self.`を追加
+    in :div
+      self.div(x, y) # `self.`を追加
+    end
+  end
+
+  def self.add(x, y) = x + y
+  def self.div(x, y) = x / y
+
+  private_class_method :add, :div # わかりやすいようにここに移動
+
   private

   def calc(x, y, type)
     case type
     in :add
       add(x, y)
     in :div
       div(x, y)
     end
   end
-  module_function :calc

   def add(x, y) = x + y
   def div(x, y) = x / y
-  module_function :add, :div
-  private_class_method :add, :div
 end

何がフクザツ?(どういう人向けにこの記事を書いた?)

本記事で問題にしている内容は、モジュールでなくクラス定義の際には、やらかしようがないように思えます。

class Kid
  def self.birth_year_in_reiwa
    birth_year - 2018 # 「どのKidインスタンスのbirth_yearを使うつもり?特異メソッドから呼べないでしょ普通」
  end

  def initialize(age)
    @age = age
  end

  def birth_year
    Time.now.year - @age
  end
end
class BaseballStats
  def self.is_fastball?(kph)
    self.mph_from_kph(kph) > 90
  end

  def mph_from_kph(kph) # 「明らかにself忘れのミス。中身的にもインスタンスメソッドじゃないし」
    kph / 1.60935
  end
end

そこで振り返ったところ、モジュール定義内にメソッドを定義する場合、

  • メソッドを書いている際のレシーバへの意識が、クラス定義の場合よりかは薄くなる(と思う)

    • インスタンスメソッドの場合、モジュールから直接インスタンスを作れないので
    • そもそも、モジュールに集めるメソッドは、statelessなものが大部分なので
  • module_functionでぺぺっと特異メソッドに昇格(?)させることができる

    • 元となるインスタンスメソッドを書いている時は、インスタンスメソッドとしての連携しか意識しなかったりすることもある
  • 「クラス定義とモジュール定義って、別物の仕組みで動いてるのかな(しらんけど)」のようなテキトーな認識だと、クラスのときと同じような注意ができない

    • 加えて「module_functionでいい感じになる」という、曖昧の重ね技が決まると尚更

というような「なんとなく」な要素があると、本記事で問題にしたつまずきとして顕現しうるのかなぁと思いました。

もし当てはまる場合は、前節の意識しておきたいことに書いたことを確認して、あれこれいじってみると、理解が深まると思います。

参考

脚注
  1. プロを目指す人のためのRuby入門[改訂2版] p.339など ↩︎

Discussion