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" が「ブロックを手続き型のオブジェクトにしたもの」を表しています。

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

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

https://docs.ruby-lang.org/ja/latest/doc/spec=2fdef.html#method

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

大事なのは、以下の 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 にぴったりくると思われる意味は、

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


これでしょう。先ほどの例を 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 がわかりにくいのか
私が思うに、yield がいまいち理解できなかった理由は、

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

からだと思います。後者の hogehoge の場合は、「&procてのが渡されてくるねんなぁ」ていうのが定義の中で直感的にわかるんですが、前者のように yield 使っちゃうとブロックが渡されるメソッドなのか、パッとはわからない。

yield に慣れてない人には特にパッとわかりにくい。

おわりに

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

ツッコミとか、コメントとか、歓迎。

ここから先は

0字

¥ 100

期間限定!PayPayで支払うと抽選でお得

この記事が気に入ったらチップで応援してみませんか?