&演算子と、procと、Object#method について理解しなおす

  • 77
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

事の始まり

やってみる

irb(main):001:0> [1,2,3].each(&method(:puts))
1
2
3

おお、うごいた!!
でもこれ何やってるの?

まずは & 演算子について

色んな意味があるけど、今回は以下の用法で使っている

xxx(&b)

Proc オブジェクトをブロックとして使う。メソッド呼び出し(super・ブロック付き・yield)/ブロック付きメソッド呼び出し を参照。

Rubyで使われる記号の意味(正規表現の複雑な記号は除く)

&演算子を使うと、procオブジェクトをブロック引数の代わりにメソッドに渡せる

次にProcオブジェクトについて

ブロックの部分だけを先に定義して変数に保存しておき、後からブロック付きメソッドに渡すことも出来ます。 それを実現するのが手続きオブジェクト(Proc)です。 それをブロックとして渡すにはブロック付きメソッドの最後の引数として `&' で修飾した手続きオブジェクトを渡します。

メソッド呼び出し(super・ブロック付き・yield)

procって何の略称?

procedureの略

proc の意味 - IT英語辞書 codic

プロシージャ (procedure)とは、プログラミングにおいて複数の処理を一つにまとめたものをいう。手続きとするのが定訳である。一連の処理を意味を持った一まとまりにすることで、再利用性が高まり、プログラム中に繰り返して現れる処理を一ヶ所で記述でき、プログラムの管理を容易にする。

プロシージャ - Wikipedia

procは、ブロック引数に渡す処理を先に記述する仕組みで、これにより再利用性が高まる

Procのサンプル

p = Proc.new {|s| s.to_s}
[:aaa, :bbb, :ccc].map(&p) #=> ["aaa", "bbb", "ccc"]

これ、こういう書き方できるよね?

[:aaa, :bbb, :ccc].map(&:to_s) #=> ["aaa", "bbb", "ccc"]

これだと、Procの代わりにシンボルを渡しているけど、どういうことなの?

to_proc メソッドを持つオブジェクトならば、`&' 修飾した引数として渡すことができます。デフォルトで Proc、Method オブジェ クトは共に to_proc メソッドを持ちます。to_proc はメソッド呼び出し時に実 行され、Proc オブジェクトを返すことが期待されます。

メソッド呼び出し(super・ブロック付き・yield)

Symbol#to_proc

self に対応する Proc オブジェクトを返します。

生成される Proc オブジェクトを呼びだす(Proc#call)と、 Proc#callの第一引数をレシーバとして、 self という名前のメソッドを 残りの引数を渡して呼びだします。

class Symbol

やってみる

:to_s.to_proc.call(:abc) # => "abc" (:abc.to_s が呼ばれる)
[:a, :b, :c].map(&to_s.to_proc) #=> ["a", "b", "c"]

Procオブジェクトのまとめ

  • ブロック引数には、&演算子を使ってブロックの代わりにProcオブジェクトを渡すことができる
  • ブロック引数には、Procオブジェクト以外にも to_proc メソッドを持つオブジェクトを渡すこともできる
  • Symbol#to_procは、selfにあるシンボルと同名のメソッドを呼び出すProcオブジェクトを生成する

最後にObject#method について

オブジェクトのメソッド name をオブジェクト化した Method オブジェクトを返します。

instance method Object#method

メソッドオブジェクト

Object#method によりオブジェクト化され たメソッドオブジェクトのクラスです。

メソッドの実体(名前でなく)とレシーバの組を封入します

class Method

やってみる

class Counter
  attr_reader :count
  def initialize
    @count = 0
  end
  def inclement
    @count = +1
  end
end

foo = Counter.new
m = foo.method(:count)
p m.call #=> 0

foo.inclement
p m.call #=> 1

Method オブジェクトも to_proc メソッドを持っているのでブロックに渡すことが可能

class Counter
  attr_reader :count
  def initialize
    @count = 0
  end
  def add(cnt)
    @count += cnt
  end
end

cnt = Counter.new
m = cnt.method(:add)
m.call(10)
p cnt.count #=> 10

10.times(&m)
p cnt.count #=> 55

Object#method の使い道って?

例えば、何かしらの処理の結果を受け取りたい時とか
普通はダックタイピング的にメソッドを定義するけど、メソッドオブジェクトを渡すようにすれば、メソッド名を気にする必要がなくなる

# 何かしらの処理
#
def compute(recelver)
  # 結果を引き渡す
  result = "結果"
  recelver.after_compute(result)
end


class Foo
  # この場合、結果を受け取るメソッド名は固定
  def after_compute(result)
    p result
  end
end

f = Foo.new
compute(f)
# 何かしらの処理
#
def compute(recelver)
  # 結果を引き渡す
  result = "結果"
  recelver.call(result)
end

class Foo
  def recelver(result)
    p result
  end
end

f = Foo.new

# Object#method を使えば結果を受け取るメソッドを自由に指定できる
compute(f.method(:recelver)) #=> "結果"

メソッドオブジェクトまとめ

  • Object#method でオブジェクトにあるメソッドをオブジェクトにできる
  • メソッドオブジェクトには、to_proc があるので、&演算子でブロック引数に渡すことができる
    • メソッドをブロック引数に渡すことができる!

[1,2,3].each(&method(:puts)) について

  • method(:puts) は、Kernel#puts のメソッドオブジェクト化
  • each(&method(:puts)) で、each のブロック引数に、Kernel#puts のメソッドオブジェクトを渡している

なので、

[1,2,3].each(&method(:puts))

は、

[1,2,3].each {|i| puts i }

と等しい!

まとめ

ブロック引数にProcオブジェクトを渡したい時は、&演算子を使う

func = Proc.new{|i| i*i}
[1,2,3].map(&func) #=> [1,4,9]

配列の要素が持つメソッドを呼ぶ場合 & 演算子と、to_proc メソッドの組み合わせを使う

[1,2,3].map(&:to_s) #=> ["1","2","3"]

他のオブジェクトが持つメソッドを使いたい場合、Object#method を使う

[1,2,3].each(&method(:puts)) #=> "1" "2" "3"

参考

以上、@hoshino さんありがとうございました!!