LoginSignup
169
142

More than 5 years have passed since last update.

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

Last updated at Posted at 2014-05-02

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

169
142
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
169
142