Tech Racho エンジニアの「?」を「!」に。
  • 開発

Rubyスタイルガイドを読む: 文法(5)ブロック、proc

こんにちは、hachi8833です。

「Rubyスタイルガイドを読む」シリーズの文法編もいよいよ第5回目にさしかかりました。今回はブロックとProc関連です。

文法(5) ブロック、procなど

2-32【統一】ブロックの各要素でメソッドを1つだけ呼び出す場合は、proc呼び出しのショートハンド「&:」を使う

Use the proc invocation shorthand when the invoked method is the only operation of a block.

Rubyを使い始めた頃はこの&:という書き方にちょっと戸惑いましたが、メソッドが1つの場合にブロックやブロック変数を書かなくて済むので慣れると便利ですね。

# 不可
names.map { |name| name.upcase }

# 良好
names.map(&:upcase)

なお&:を呼び出すには、そのメソッド(上の例ではupcase)にSymbol#to_procが実装されている必要があります。

当然ながら、&:記法ではブロック変数を使えません。

参考

2-33【統一】ブロック記法: 1行に収まる場合は原則{...}を使い、制御フローやメソッド定義では常にdo...endを使う

Prefer {...} over do...end for single-line blocks. Avoid using {...} for multi-line blocks (multi-line chaining is always ugly). Always use do...end for "control flow" and "method definitions" (e.g. in Rakefiles and certain DSLs). Avoid do...end when chaining.

do...endの後ろにドット.でメソッドチェーンするのは見苦しいので避けるようにとのことです。

names = %w(Bozhidar Steve Sarah)

# 不可
names.each do |name|
  puts name
end

# 良好
names.each { |name| puts name }  # 1行なら { } で書く

# 不可
names.select do |name|
  name.start_with?('S')
end.map { |name| name.upcase }   # do...endに続けてメソッドチェーンしないこと 

# 良好
names.select { |name| name.start_with?('S') }.map(&:upcase)

Some will argue that multi-line chaining would look OK with the use of {...},
but they should ask themselves—is this code really readable and can the
blocks' contents be extracted into nifty methods?

「複数行を{...}ブロックで囲んでメソッドチェーンするなら見苦しくはならないのでは?」という反論については、「そもそもブロックにコードを何行も書かなければならないなら別途メソッド化すべき」という立場を取っています。

試しに以下のように書き換えてみると、「1行に収まる場合は原則{...}を使う」というスタイルとの共存が面倒になるのがわかります。

names.select { |name| 
  name.start_with?('S')
  ...
  ...
}.map(&:upcase)

ルールを制定する側は例外事項をなるべく避けたいはずなので、「1行に収まる場合は原則{...}を使う」を優先したのだろうと推測しました。

これに限りませんが、目安程度に考えるほうがよいスタイルもいくつかあります。指示に盲目的に従うよりも、時に応じ場合に即して使い分けたいものです。

2-34【統一】引数を別のブロックに渡すだけのブロックは、明示的なブロック引数で書き直す

Consider using explicit block argument to avoid writing block literal that just passes its arguments to another block. Beware of the performance impact, though, as the block gets converted to a Proc.

ブロックを&blockなどのブロック引数で受けることで、余分なブロックもyieldも使わずに済み、読みやすくなりますね。

require 'tempfile'

# 不可
def with_tmp_dir
  Dir.mktmpdir do |tmp_dir|
    Dir.chdir(tmp_dir) { |dir| yield dir }  # 引数を渡すためだけにブロックとyieldを余分に使っている
  end                                       # その代わりProcオブジェクトへの変換は発生しない
end

# 良好
def with_tmp_dir(&block)                    # ブロック引数「&block」で受けることでブロックを1つ減らせる
  Dir.mktmpdir do |tmp_dir|
    Dir.chdir(tmp_dir, &block)              # その代わりブロックを&blockに保存するときにProcへの変換が発生する
  end
end

with_tmp_dir do |dir|
  puts "dir is accessible as a parameter and pwd is set: #{dir}"
end

その代わり、ブロックをブロック引数&blockに保存するときにProcオブジェクトに変換されるので、パフォーマンスとのトレードオフであることを理解して使いましょう。

上で不可とされている書き方では、ブロックをコード中で直書きしているので、Procオブジェクトへの変換は発生しません。

2-35【統一】不必要なreturnは避ける

Avoid return where not required for flow of control.

これはRubyのスタイルとして有名ですね。Rubyでは最後に実行した文の値が戻り値になるので、多くの場合returnは不要です。

# 不可
def some_method(some_arr)
  return some_arr.size
end

# 良好
def some_method(some_arr)
  some_arr.size
end

2-36【統一】不必要なselfは避ける

Avoid self where not required. (It is only required when calling a self write accessor.)

これは同じクラスにある他のメソッドを呼ぶ場合の話ですね。書き込みアクセサの呼び出しの場合のみselfを付けるようにします。

# 不可
def ready?
  if self.last_reviewed_at > self.last_updated_at
    self.worker.update(self.content, self.options)
    self.status = :in_progress
  end
  self.status == :verified
end

# 良好
def ready?
  if last_reviewed_at > last_updated_at
    worker.update(content, options)
    self.status = :in_progress               # このself.は略さない(書き込みアクセサの呼び出しであることを示す)
  end
  status == :verified
end

実際、上のコードはselfなしでも動作します。書き込みアクセサの呼び出しでselfを付けるのは、そうしないとstatusという変数を新規作成して代入することになってしまうためです。

2-37【統一】ローカル変数でメソッドをシャドウイングすることは避ける

As a corollary, avoid shadowing methods with local variables unless they are both equivalent.

以下の例では、optionsというアクセサがあり、さらに#initializeメソッドの仮引数であるローカル変数も同じ名前のoptionsになっています。

これは確かに紛らわしいので、避けたいです。

class Foo
  attr_accessor :options

  # ok
  def initialize(options)
    self.options = options
    # ここではoptions と self.optionsが同等になっている
  end

  # 不可
  def do_something(options = {})         # このoptionsの名前を変えるべき
    unless options[:when] == :later
      output(self.options[:message])     # そうすればこのselfは不要になる
    end
  end

  # 良好
  def do_something(params = {})
    unless params[:when] == :later
      output(options[:message])
    end
  end
end

今回はここまでとします。次回は等号=などの演算子関連のスタイルです。ご期待ください。

関連記事



CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。