よしだの自習室

Rubyのyieldって結局何なの?

本記事は、2010年ごろに「はてなダイアリー」で私が執筆したものを一部修正したものです。 当時とは p メソッドの戻り値など微妙に仕様が変わっている模様。。

Rubyのyieldって結局何なの?

yield って、いまいち分かりにくいですよね。。

この記事は Ruby の yield に関して、

わからない⇒調べる⇒忘れる⇒調べる⇒忘れる⇒…

のエンドレスループから抜け出すために、自分なりにまとめたものです。

ブロック

いきなり yield じゃないやん!という感じですが、我慢して見てみてください。

じつは、Ruby のメソッドはすべて「ブロック」を引数にすることができます。

def hogehoge( x )
  return x + 2
end

p hogehoge( 3 )
p hogehoge( 5 ){ p "foo" }

ブロックってのは、{ p “foo” } みたいに “{“ と “}” に囲まれたやつね。”do” ~ “end” でもいいみたいだけど。

これを実行すると、

5
7

となります。{ p “foo” } はまるっきりシカトです。

(・∀・)

…が、以下のようにすると、様子が違ってきます。

def hogehoge( x )
  p block_given?
  return x + 2
end

p hogehoge( 3 )
p hogehoge( 5 ){ p "foo" }

# --- 結果 ---
false
5
true
7

あるメソッドにブロックが与えられているかどうかは、そのメソッド内で “block_given?” という値を参照することにより確かめることができます。

では、メソッドに渡されたブロックはいかにしてメソッドに影響を与えることができるのでしょうか?

次のコードを実行してみてください。

def hogehoge( x, &proc )
  proc.call if block_given?
  return x + 2
end

p hogehoge( 3 )
p hogehoge( 5 ){ p "foo" }

結果は次のようになるはずです。

5
"foo"
7

これは、「メソッドにブロックが与えられていれば、そのブロックを実行しなさいよ~」ということです。

少しややこしいですが、”&proc” というのが「ブロック」を表していて、”&” を取り除いた “proc” が「ブロックを手続き型のオブジェクトにしたもの」を表しています。

ちなみにメソッドを定義するとき、ブロックは必ず最後の引数にしなければなりません。そして上記のように、無きゃ無いで無視されます。このへんはデフォルト引数に似ていますね。

メソッド定義の詳細は、オフィシャルの以下を参照ください(^_^;;

クラス/メソッドの定義 (Ruby 3.0.0 リファレンスマニュアル)

ここでは「メソッドへブロックを渡す」ということについて書きました。

大事なのは、以下の 2 点です!

  • 「メソッドにはブロックを渡すことができる」
  • 「渡したブロックはメソッド内で手続き型のオブジェクトとして扱われる」

手続き型オブジェクト

まだ yield が出なくてごめんなさい。

まず、手続き型オブジェクト (=Procクラスのインスタンス) について簡単にまとめます。Rubyリファレンスマニュアルによると、

Proc はブロックをコンテキスト(ローカル変数のスコープやスタックフレーム)とともにオブジェクト化した手続きオブジェクトです。

ということです。

さっぱり意味がわかりませんちょっと意味がわかりにくいので、例をあげてみようと思います。

prci = Proc.new{ p "hoge" }

p prci.class
prci.call

# --- 結果 ---
Proc
"hoge"

こんなふうになります。prci は { p “hoge” } の働きを持つ「インスタンス」で、クラスは Proc であるということになります。

これは以下の表現に似ています。

def prcm
 p "hoge"
end

p prcm.class
prcm

# --- 結果 ---
String
"hoge"

二つの動作は同じで、表現方法も似ていますが、prcm は「メソッド」でありインスタンスではありません。prcm は { p “hoge” } の戻り値 (“hoge”) を返すので、 prcm.class は String となります。

実際の動作は同じですが、動作させるときには

prci.call # Proc のインスタンス (手続き型オブジェクト) prcm # メソッド という違いがあることに注意してください。

メソッドはもちろんですが、手続き型オブジェクトにも引数を与えることができます。以下の例を見てください。

prci2 = Proc.new{|a, b| ( a + b ) }
prci3 = Proc.new{|a, b| ( a + b ); ( a - b ) }

def prcm2( a, b )
 return ( a + b )
end

p prci2.call( 3, 5 )
p prci3.call( 3, 5 )
p prcm2( 4, 9 )

# --- 結果 ---
8
-2
13

手続き型オブジェクトを実行したときの値 ( メソッドで言うと戻り値 ) は、ブロック内で最後に評価された値になります。

だから prci2.call( 3, 5 ) の値は 8、prci3.call( 3, 5 ) の値は -2 となるわけですね。

手続き型オブジェクトについて、ざくっとおわかりいただけたでしょうか。

yield

“yield” を英和辞典で引いたときに、Ruby における yield にぴったりくると思われる意味は、

  1. 〔ほかのものに〕取って代わられる

これでしょう。先ほどの例を yield を使わない場合、使う場合でそれぞれ以下のように表現することができます。まずは使わない場合。

def hogehoge( x, &proc )      # &proc
  proc.call if block_given?   # proc.call
  return x + 2
end

p hogehoge( 3 )
p hogehoge( 5 ){ p "foo" }

# --- 結果 ---
5
"foo"
7

yield を使う場合。

def hogehoge( x )             # &proc はない!
  yield if block_given?       # proc.call が yield に!
  return x + 2
end

p hogehoge( 3 )
p hogehoge( 5 ){ p "foo" }

# --- 結果 ---
5
"foo"
7

前者と後者は全く同じふるまいをします。

つまり、yield は渡されているブロックと同じ働きをするメソッドのようなものなのです!

「引数の部分に &proc を書く」⇒「proc.call で呼び出す」 という動作を、

「yield で呼び出す」 と書くことができるわけです。

この場合、yield は { p “foo” } の処理をする手続き型オブジェクトに「取って代わられる」ことになります。

雰囲気、つかめましたか?

なぜわかりにくいのか

私が思うに、yield がいまいち理解しにくい理由は、

「メソッド定義の外側にある情報(=ブロック)を暗黙的に利用する」

からではないでしょうか。

メソッド定義に注目すると、先ほどの例で &proc を実際に引数として渡す場合は「手続き型のオブジェクトが渡されてくるんだな」ということがメソッドの引数を定義する箇所で理解できるんですが、yield を使う場合は 「コード内で yield が登場した時点でブロックが渡されるメソッドということがわかる」 ということになります。

yield に慣れてないと、余計にわかりにくいですよね。

本記事の例などを通じて理解したうえで、yield を使うもよし、代替手段があるということで &proc を使うもよし。いろいろな書き方ができるのも Ruby の楽しいところかと思います(^ ^)/

それでは、今回はこの辺で。

[BACK TO TOP]

コメント

コメントはまだありません。

コメントには GitHub のアカウントが必要です。

コメントを書く
[BACK TO TOP]