Edited at

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

More than 1 year has passed since last update.

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