[Ruby] モジュール関数から、同じモジュールのプライベートメソッドを呼び出す
モジュールで、「他のインスタンスメソッド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_functionで一気に定義できるやつ
-
モジュール関数=モジュールの特異メソッド+モジュールのプライベートメソッド;
-
「モジュールのプライベートメソッド」->「モジュールのインスタンスメソッド(パブリック/プライベート関係なし)」
- (『モジュール』内で定義する通常のメソッドも『インスタンス』メソッドと呼ぶ、というのが出てこない人向けの説明です)
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 #=> "インスタンスメソッド"
まとめ
-
モジュールの特異メソッド内から、同じモジュールに定義された(純粋な)インスタンスメソッドを呼び出すことはできない
- 事情としては、クラス定義における、特異メソッド(クラスメソッド)とインスタンスメソッドとの関係と同じ。メソッドのレシーバが異なる
-
呼び出したいと思っているインスタンスメソッドを、特異メソッド化する(特異メソッドにも生やす)必要がある
- 単純には、
module_function <instance_method>すればよい - あるいは、
module_functionしたあとにprivate_class_methodすれば、特異メソッドとしてもプライベート化できる
- 単純には、
-
モジュール定義特有の
module_functionは、「同名・同機能のパブリック特異メソッドを定義する」「それと、インスタンスメソッドをプライベートにする」をしているだけ- 手で再現できる
- クラス定義で使う
attr_accessor系と同じ
モジュールの特異メソッドからは、同じモジュール内に定義したインスタンスメソッドを呼び出せない
モジュールの特異メソッドは、
-
まずインスタンスメソッド
fooを定義する -
適宜、連携する別のインスタンスメソッド
barも定義する -
1の
fooに対してmodule_function :fooを使って、fooをモジュール(仮にBaz)の特異メソッドにもする
というステップで定義することも多いと思います。
このとき、fooを特異メソッドとして呼び出す(Baz.foo)と、インスタンスメソッドfoo, barをまず定義したときの認識とは裏腹に、Baz.fooがbarを呼び出せないのでエラーになってしまいます。
具体的には、以下のようなことをしようとすると、失敗してしまいます。
(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としてaddやdivを呼び出すのは、(想定通り)問題ありません。これはこのままでよいとします。
# - 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のパブリックな特異メソッドにしたい -
addとdivはプライベートインスタンスメソッドにしたい- つまり、モジュール
Calculatorの(外から呼べる)特異メソッドにはしたくない -
includeされた先でプライベートインスタンスメソッドになるのは別にいいよ
- つまり、モジュール
を満たしたまま、
- でもでも、
calcからaddとdivを呼び出して利用したい
という場合、どうすればよいでしょうか。
対応方法
結論から先に言うと、特異メソッドから呼び出したいならば、呼び出したい(名前と中身の)メソッドを特異メソッドに用意するしかありません。
手でコピペ&調整で定義するのは冗長かつ危ないので、以下のようにします。
module_function+private_class_methodの方法
今回の場合、addとdivに対して、
-
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と同じような形で使っていました(なるほど!)。
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.foo+private_class_methodの代わりに、class << self+privateでもよい
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がやっていることを自分の手でやってみよう、ということでもあります。
モジュール定義をクラス定義に置き換えてみる
具体的には、以下のようにします:
-
モジュール定義の
moduleをclassに置き換える -
module_functionで定義した特異メソッドfooを、同名で手で明示的に定義(def self.foo)しなおす- このとき、メソッド内の他メソッド
barの呼び出しは、全部同名の特異メソッドの呼び出しself.barに置き換える - (インスタンス変数...もクラス変数なりクラスインスタンス変数なりに置き換えるが、別問題として、そもそもモジュールのインスタンスメソッド内でインスタンス変数を使うこと自体があまりよろしくない設計(
include先のインスタンス変数をいじってしまうため)[1]なのでスルー)
- このとき、メソッド内の他メソッド
-
module_functionを消す- このとき、インスタンスメソッドに
private設定を(なければ)する(一応)
- このとき、インスタンスメソッドに
-
private_class_methodを、手で定義した特異メソッドの近くに移動する(見やすさのため)
具体例として、本記事最初の方で示したNG例・OK例それぞれの置き換えをやってみましょう。
NG例を置き換えてみる
オリジナルでは、calcのみにmodule_functionを適用していたので、
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
こうします:
-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, divにmodule_functionを適用していました。
また、self.add, self.divに対しては、private_class_methodが適用されています。
これらを踏まえると、
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
こうなります:
-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でいい感じになる」という、曖昧の重ね技が決まると尚更
- 加えて「
というような「なんとなく」な要素があると、本記事で問題にしたつまずきとして顕現しうるのかなぁと思いました。
もし当てはまる場合は、前節の意識しておきたいことに書いたことを確認して、あれこれいじってみると、理解が深まると思います。
参考
-
instance method Module#private_class_method (Ruby リファレンスマニュアル)
-
クラス/メソッドの定義 (Ruby リファレンスマニュアル)
一応ここに載せているけれども、このページの理解の助けにはならない可能性大(このページよりも、もうちょっとプリミティブで突っ込んだ部分になるので)
-
プロを目指す人のためのRuby入門[改訂2版] p.339など ↩︎
Discussion