なまもの備忘録

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

ブロック付きgsubの返り値の違いについて

基本的な話なのだがRubyのブロックには二通りの記法がある。

Objects.each { |object|
    object.method
}

Objects.each do  |object|
    object.method
end

だ。

僕は、手続き上両記法は等価なものだと思っていたのだが、どうもそうでは無いらしい、ということが分かったのでメモとして残しておく。

違い

さて、どのようなケースで等価ではないかというと、下記のような時だ。

pry(main)> s = "hello, world"
pry(main)> puts s.gsub(/\w+/) {|word| word.capitalize }
Hello, World
=> nil

このコードをdo...endによる記法で書くと以下の様な挙動になる。

pry(main)> s = "hello, world"
pry(main)> puts s.gsub(/\w+/) do |word|
pry(main)*   word.capitalize
pry(main)* end
#<Enumerator:0x000056474a99ef70> 
=> nil 

原因

原因は、{...}do...endの結合の強さの違いにある。 Ruby 2.4.0 リファレンスマニュアルには下記のようにある

{ ... } の方が do ... end ブロックよりも強く結合します。 次に例を挙げますが、このような違いが影響するコードは読み辛いので避けましょう:

foobar a, b do .. end # foobarの引数はa, bの値とブロック
foobar a, b { .. } # ブロックはメソッドbの引数、aの値とbの返り値とがfoobarの引数

つまり、

pry(main)> s = "hello, world"
pry(main)> puts s.gsub(/\w+/) do |word|
pry(main)*   word.capitalize
pry(main)* end
#<Enumerator:0x000056474a99ef70> 
=> nil 

では、putsメソッドの引数はs.gsub(/\w+/)do...endブロックと解釈されているのだ。

それぞれ、分けて実行してみると、以下のようになる

pry(main)> puts s.gsub(/\w+/)
#<Enumerator:0x0000560d6a307a78>
=> nil
pry(main)> puts do
pry(main)*   s = 10
pry(main)* end

=> nil

なるほどこの2つが合わさっていたのか。

Rubyではブロックを取るメソッドをブロックを省略して書いた場合、Enumeratorオブジェクトを返す。 gsubの引数に正規表現パターンのみを与えた場合、ブロックを使っての置換の記法になるため、1つめはこれだろう。

2つめはなんだろうか。 チラッと調べてみた所、ブロックを取らないメソッド(内部にyieldがない)にブロックを渡した場合、ブロックは単に無視される、というが引っかかった。 多分これなんじゃないだろうか。こっちについてはもう少し確認する必要がありそうだけれども。

バージョン

$ ruby --version
ruby 2.4.2p198 (2017-09-14 revision 59899) [x86_64-linux]
=> nil 

参考

Enumerator とブロックの省略 - まめめも

ブロックをdo…endで書くか{…}で書くかにより挙動が変わる例 - Qiita

ブロックを与えない場合にEnumeratorを返すメソッドを作る - Qiita