Ruby

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

Ruby - Nokogiriによるスクレイピング(YahooFinance) - Qiita
のコメント欄を書いているときに、挙動の違いに改めて気付いたので挙げときます。
Enumerable#injectに限られる話ではないのですが、気付いたときに使っていたのがこのメソッドだったので。

先に結論を

do…endで書くときには、返値確認用のpを直接使うな。

Enumerable#inject

まず中括弧の場合。injectの例としてこちらから引用します。
ruby の inject をわかりやすく説明してみる - Λάδι Βιώσας

pry(main)> p (1..10).inject(0) {|sum, i| sum + i }
55
=> 55

初期値0をsumに入れて、返値sum + iをsumに戻して次のループへ、ってメソッドですね。
ここで使っている中括弧を単純にdo…endに変えます。

pry(main)> p (1..10).inject(0) do |sum, i|
pry(main)*   sum + i  
pry(main)* end  
TypeError: 0 is not a symbol
from (pry):**:in `inject'

…え?
そうだよ「0はシンボルじゃない」よ。当然じゃないか。
なんでそんなエラーを返すの??

るりまを確認

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

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

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

つまり、括弧を明示すると

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

とRubyインタプリタは解釈します。

考察

pを使うときには一般的に、引数を囲む括弧を用いない。
確認のために括弧を明示してみる。

括弧無し
p (1..10).inject(0) do |sum, i|
  sum + i
end

上記のことを考慮すると

括弧付き
p((1..10).inject(0)) do |sum, i|
  sum + i
end

と解釈されてそう。

pry(main)> p((1..10).inject(0)) do |sum, i|
pry(main)*   sum + i  
pry(main)* end  
TypeError: 0 is not a symbol
from (pry):**:in `inject'

pry(main)> p((1..10).inject(0))
TypeError: 0 is not a symbol
from (pry):67:in `inject'

ビンゴ。

るりまのEnumerable#injectを確認

inject(init = self.first) {|result, item| ... } -> object[permalink]
inject(sym) -> object

[PARAM] sym:
ブロックの代わりに使われるメソッド名を表す Symbol オブジェクトを指定します。 実行結果に対して sym という名前のメソッドが呼ばれます。
instance method Enumerable#inject

理解

  1. do…endだと{…}よりも結合度が低いので、pを使うと(1..10).inject(0)までがpの引数として扱われた。
  2. そのためにinjectの引数が「Symbolじゃない」とエラーが出た。
  3. うしろに付いているdo…endブロックは無視されている。

解決例

result = (1..10).inject(0) do |sum, i|
  sum + i
end
p result

まとめ

大事なことなので二度書きます。
do…endで書くときには、返値確認用のpを直接使うな。

はてブコメントへの返答

id: mattn

まとめが逆な気がした。「pを使う時は do end は使うな」?

Kernel.#pは基本的にデバッグ用のメソッドです。
つまり、ここでpを使ったのは 返値を一時的に知りたいから であり、最終的にpは削除します。
という心持ちでこのエントリを書いたので、do…endが主体になっています。
また、「直接」と書いたのは 返値を知りたいならば、一時変数に代入してからpで表示すべきであり、一時変数を省いてpを使うと痛い目に遭う という意味合いでした(分かりづらかったので「解決例」の段落を追加しました。

id: ngsw

http://www.oki-osk.jp/esc/ruby/tut-07.html ここらと同じなのだろうか

ご指摘の通りです。
上記サイト
Ruby チュートリアル - 7. 微妙な問題
から一部を引用します。

7.5 do … end ブロックの優先順位

p [1,2,3].each {|x| puts x}, 4  # 無事に実行される
p [1,2,3].each do |x| puts x end, 4  # SyntaxError

…カンマが後続することは構文的にあり得ないから…

p [1,2,3].each do |x| puts x end  # LocalJumpError

なぜだろうか?…つまり,括弧で明示すると

p([1,2,3].each()) {|x| puts x}  # LocalJumpError

であるかのように構文解析される。 たしかに SyntaxError ではないが,イテレータである each にブロックが渡されないから, LocalJumpError が発生する。

まさにdo…endと{…}との挙動の違いです。

id: otchy210

Rubyist では無いので詳しくないのだけど、よく色んな言語で見かける、&& と and の強さの違いみたいのが、ブロックでもあるっていう事か。

ご指摘のように、演算子の優先順序に近いものがあると思います。

  • Rubyはメソッドの引数を明示する括弧を(意味が不明瞭にならない範囲で)省くことが出来る
  • 標準出力へ出力するメソッド(puts, p, pp, print, …)では、括弧を省くのが通常の書き方

この二点が影響して(私的なミスが生じて)今回のエントリを書くことになったわけですw

id: tanakaBox

割と色々ある気がする。この場合、(1..10).inject(0) do |sum, i|; sum + i; end.tap{|t| p t}と、endの後に続ければいいんじゃね?

今回の目的を満足するためには、全くその通りです!知らなかったので感謝です!
気になることとしては、すこし話がずれるのですが、end.tapというふうに、endのあとにメソッドを続けることに違和感が生じること、でしょうか。(パーフェクトRuby pp.101-2にも、do…endと{…}の書き分け理由の一つに挙げられてました。

後日追記:
末尾に Object#tap メソッドを使わなくても Object#display メソッドがあるじゃないか。
忘れられた出力メソッドObject#display - Qiita