なまもの備忘録

気になったことをつらつらと書いていきます

関数的メソッドをメソッド引数として渡す

関数的メソッドを引数にとる関数の定義がしたかったのだが、調べてもなかなか丁度良い記事がなかったのでメモ代わりに。 関数的メソッドとは明示的なレシーバを持たないメソッドのこと。素直にグローバルスコープにメソッドを定義するとこれになる。

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#methodModule#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 が「グローバルスコープ」だからなのではなく、 FooObject を継承してるから、というのが真相です。

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) がうまく動いたのも、ClassObject を継承しているからです。

余談ですが、pputs などの多くの組み込み関数も似たような仕組みになっています。実はこれらは Kernel クラスのメソッドとして定義されていて、 Object はこの Kernel を継承しています。なので、test_func と同様に pputs もいろんな場所から呼べるというわけです。

適切なレシーバーは何なのか

ここまでを踏まえてようやく本題です。

案1. BasicObject のインスタンス

とりあえず記事内での考えを延長して、以下のようなコードを考えたくなるかもしれません。しかしこれはエラーになります。

func = BasicObject.new.method(:test_func) # => undefined method `method' for #<BasicObject:0x00007f9c988641b0> (NoMethodError)

理由はもちろん、test_funcObject のメソッドだからであり、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. そもそもメソッドを使わない(一番オススメ)

レシーバーが必要なのはメソッドだからです。Proclambda を使えばレシーバー無しの「関数」が扱えます。

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 }

以上、コメントで長々と書いてしまいましたが、参考になりましたら幸いです。