関数的メソッドをメソッド引数として渡す
関数的メソッドを引数にとる関数の定義がしたかったのだが、調べてもなかなか丁度良い記事がなかったのでメモ代わりに。 関数的メソッドとは明示的なレシーバを持たないメソッドのこと。素直にグローバルスコープにメソッドを定義するとこれになる。
def test_func(a,b,c) p a + b + c end def test_func_func(func) func.call(1,2,3) end func = BasicObject.method(:test_func) test_func_func(func) # 6 # => 6
どうもにメソッドに引数としてメソッドを渡すときはObject#methodを使ってメソッドを一度オブジェクト化する見たいなのだが、Object#methodは呼び出し時にメソッドのレシーバを明示する必要がある。 では関数的メソッドのレシーバとして記述するオブジェクトは何が適切なのか、という話になるが、これについて明確にこれが良い、というものは分からなかった。 例には、取り敢えず余計な要素を持たないBasicObjectなんか適しているんじゃないだろうか、という推測のもとBasicObjectを用いている。 どなたかご存知でしたら教えてください。
20/2/3追記
id:ikngttyさんがコメントで下記のことを教えてくださいました。
- トップレベル(グローバルスコープ)に定義されたメソッドはObjectのprivateインスタンスメソッドとして定義される。
- トップレベルでの挙動は
self.test_func(args...)
に相当(selfはmainを指す)する。 - selfは省略可能なので、
method(:test_func)
でのオブジェクト化が可能。
ですので、上記を踏まえると以下の書き方が適切に思えます。
def test_func(a,b,c) p a + b + c end def test_func_func(func) func.call(1,2,3) end func = Object.new.method(:test_func) # メソッドの所属先を明示的に表記する場合 func = method(:test_func) # 簡潔に表記する場合 test_func_func(func)
id:ikngttyさんもおすすめされていますが、トップレベルでレシーバが省略されている時点Objectのメソッドであることは確定するので、selfを省略する書き方が個人的にもしっくりきます。メソッドの所属先を明示するためだけにObjectクラスのインスタンスを生成するのはかなり気が引けます。 また、こちらもid:ikngttyさんがおすすめされていますが、特にメソッドを使い回すなどしない場合であればラムダやProcが適切そうです。
いただいたコメントにとても丁寧な解説がありますので、そちらも是非ご覧下さい。
他参考:Ruby のトップレベルメソッドって結局なんなの - Qiita
20/2/25追記
id:ikngttyにいただいたコメントがMarkdown形式のものだったので、許可をいただき、コメントをそのままここに載せさせていただくことにしました。 大変丁寧な解説をありがとうございました。
以下コメントです。
取り敢えず余計な要素を持たないBasicObjectなんか適しているんじゃないだろうか、という推測のもとBasicObjectを用いている。
残念ながら、お望みの効果は得られていない状態になっています。
Object#method と Module#instance_method
Ruby には似たような役割を持つメソッドとして、Object#method
と Module#instance_method
があります。
# Object#method abs123 = -123.method(:abs) p abs123 # => #<Method: Integer#abs> p abs123.call # => 123 # Module#instance_method abs = Integer.instance_method(:abs) p abs # => #<UnboundMethod: Integer#abs> abs234 = abs.bind(-234) p abs234 # => #<Method: Integer#abs> p abs234.call # => 234
つまり、以下の2つが大体同じ関係になっています。
インスタンス.method(:name)
クラス.instance_method(:name)
これを混同して クラス.method(:name)
としてしまうと、以下のようになります。
abs = Integer.method(:abs) # => undefined method `abs' for class `#<Class:Integer>' (NameError)
本記事の
func = BasicObject.method(:test_func)
こちらも同様の構造です。
クラス.method(:name) では何が起きているのか
紛らわしい文章になりますが、クラスをインスタンスとして見た場合、それのクラスはClass
クラスです。
紛らわしいので、以下のコードを見た方がおそらく早いです。
class Foo end foo = Foo.new p foo.class # => Foo p Foo.class # => Class p BasicObject.class # => Class
なので BasicObject.method(:name)
は Class.instance_method(:name)
と意味合いが近いです。
p BasicObject.methods == Class.instance_methods # => true
これは BasicObject
に限らず全てのクラスについて言えます。
p Foo.methods == Class.instance_methods # => true
つまり、以下の2つのコードは同じということです。
func = BasicObject.method(:test_func)
func = Foo.method(:test_func)
この辺が「(BasicObject
を用いても)お望みの効果が得られてない」という根拠です。
「グローバルスコープに定義したメソッド」の正体
では改めて、何がレシーバーとして適切なのか。これは、「グローバルスコープに定義したメソッド」について詳しく調べることが近道となります。
main
オブジェクトについてのリファレンスに、この辺の挙動が詳しく書いてあります。
要約
トップレベルでの self を表すオブジェクトです。
main では参照できない事に注意してください。トップレベルで self から参照してください。
トップレベルで定義したメソッドは Object の private インスタンスメソッドと して定義されます。
(太字による強調はコメント主の私による)
例えば以下のコードで test_func
を呼べるのも、test_func
が「グローバルスコープ」だからなのではなく、 Foo
が Object
を継承してるから、というのが真相です。
def test_func(a,b,c) p a + b + c end class Foo def hoge test_func(2, 3, 4) end end Foo.new.hoge # => 9
また、BasicObject.method(:test_func)
がうまく動いたのも、Class
が Object
を継承しているからです。
余談ですが、p
や puts
などの多くの組み込み関数も似たような仕組みになっています。実はこれらは Kernel
クラスのメソッドとして定義されていて、 Object
はこの Kernel
を継承しています。なので、test_func
と同様に p
や puts
もいろんな場所から呼べるというわけです。
適切なレシーバーは何なのか
ここまでを踏まえてようやく本題です。
案1. BasicObject のインスタンス
とりあえず記事内での考えを延長して、以下のようなコードを考えたくなるかもしれません。しかしこれはエラーになります。
func = BasicObject.new.method(:test_func) # => undefined method `method' for #<BasicObject:0x00007f9c988641b0> (NoMethodError)
理由はもちろん、test_func
が Object
のメソッドだからであり、BasicObject
には含まれないからです。
案2. Object のインスタンス
じゃあ Object
なら動くのか。その通りです。
func = Object.new.method(:test_func) test_func_func(func) # => 6
案3. main オブジェクト(オススメ)
トップレベルで動かすのと全く同じ状態で渡したいなら、トップレベル(=main
オブジェクト)をレシーバーにするのが良いです。上で抜粋した通り、main
オブジェクトは self
で参照します。
func = self.method(:test_func) test_func_func(func) # => 6
なお、self
は省略できます。
func = method(:test_func) test_func_func(func) # => 6
なんと、「レシーバーなんて要らなかったんや!」というオチ。
案4. そもそもメソッドを使わない(一番オススメ)
レシーバーが必要なのはメソッドだからです。Proc
や lambda
を使えばレシーバー無しの「関数」が扱えます。
test_func = lambda { |a, b, c| p a + b + c } def test_func_func(func) func.call(1, 2, 3) end test_func_func(test_func) # => 6
ちなみに lambda
の代わりとして ->
という特殊なリテラル記法もあります。
test_func = ->(a, b, c) { p a + b + c }
以上、コメントで長々と書いてしまいましたが、参考になりましたら幸いです。