🐫

Ruby の block-pass に Symbol を渡すのはもう避けたいかもしれません

に公開

Ruby では &:to_s のように Symbol を使って block を渡す書き方が一般的によく使われます。
簡潔で美しい構文に見える一方で、初心者には少しわかりづらく、IDE やリファクタリングツールとの相性もいまひとつ。
最近では it_1 など、より直感的な書き方も登場し、チームでのスタイル統一を考える場面も増えてきました。

本記事では、こうした背景を踏まえつつ、Symbol の block-pass をあえて避ける選択肢と、RuboCop を使った実践的な対処法について考察します。

ブロックパラメータの歴史とそれぞれの特徴

最近 Ruby では it というブロックパラメータが追加されました。

# 以下はいずれも、 ["1", "2", "3"] という結果を返します。

# 一番プリミティブな書き方
[1, 2, 3].map { |i| i.to_s }

# Ruby 1.9 からできるようになった書き方
[1, 2, 3].map(&:to_s)

# Ruby 2.7 から、 Numbered Parameters が追加されました
[1, 2, 3].map { _1.to_s }

# Ruby 3.4 から、 it が追加されました
[1, 2, 3].map { it.to_s }

プリミティブな記述について

まず、以下のようなプリミティブな書き方ですが、 i という変数が使われています。
このようなごく小さなブロックにおいて、名前自体がなんでもよいのですがどうしてもそこに名前をつける必要がありました。

# 一番プリミティブな書き方
[1, 2, 3].map { |i| i.to_s }

Symbol#to_proc な記述について

そして、 block-pass に Symbol を渡すことで Symbol#to_proc が呼ばれることを前提とした以下のコードが使われるようになりました。

# Ruby 1.9 からできるようになった書き方
[1, 2, 3].map(&:to_s)

ちなみに、 Symbol#to_proc は以下のように
「引数をひとつ受け取り、そこに public_send する」みたいな挙動をします。

sym = :to_s
blk = sym.to_proc

# ->(obj, *args, **kwargs, &block) { obj.public_send(:to_s, *args, **kwargs, &block) } みたいな挙動
blk.call(1)  #=> "1"

この Symbol を、 block-pass として渡すことで暗黙的に to_proc が呼ばれるので先に期待した挙動となっているわけです。

Numbered Parameters と it

それから時は流れ、「名前はなんでもよい (名前がないことに意味がある)」がより明確に表現された Numbered Parameters と it が誕生しました。

# Ruby 2.7 から、 Numbered Parameters が追加されました
[1, 2, 3].map { _1.to_s }

# Ruby 3.4 から、 it が追加されました
[1, 2, 3].map { it.to_s }

他の言語でも Kotlin の it 、 Scala の _ とかありますね。

Symbol block-pass を避けたい理由

このように進化してきたわけですが、動機としては「名前はなんでもよい (名前がないことに意味がある)」がポイントになっていると思います。
その点については、たしかに Ruby 1.9 以降の記法はそれに対する解決になっています。

そのうえでの思いですが、私は it 推しです。
より突っ込むと、 _1 でも it でもよいと思っているのですが「それ」と表せる it は後発ですし、トレンドに逆らわないのが良いかなという無難な選択です。

Symbol#to_proc は短く書けるのですが、「否 Ruby 話者が見ると何をやっているか悩む」という点が見逃せない課題だと思っていました。
Ruby は同じ課題を解決するのに色んな記述ができて楽しいのですが、私は Ruby が流行ってほしいので初心者にとってのとっつきやすさは重要なファクタだと思っています。
また、_1it は IDE との相性もよさそうです。

そんなわけで、もう Symbol#to_proc はコードゴルフなどの特殊用途にとどめ、実用コードからは手を引いてもよい頃合いではないでしょうか。

Custom Cop

主張するだけならポエムなので、この主張を助ける Custom Cop を用意します。
Rubocop の custom cop として以下を適用すると、 array.map(&:to_s) のような記述を違反としてくれます。

Style::SymbolProc の逆みたいなものです。

# この Custom Cop を導入するには、`.rubocop.yml` に以下を追記します。
#
# require:
#   - path/to/custom/cop/avoid_symbol_block_pass.rb
#
# Custom/AvoidSymbolBlockPass:
#   Enabled: true
#
# ※ `require:` のパスは、このファイルの実際の保存場所に応じて書き換えてください。
#    たとえば `rubocop/cop/custom/avoid_symbol_block_pass.rb` に保存している場合は、
#    `"rubocop/cop/custom/avoid_symbol_block_pass"` のように記述します。
#
# --- 違反例(NG) ---
# array.map(&:to_s)
# users.each(&:destroy)
#
# --- 許容例(OK) ---
# array.map { it.to_s }
# users.each { it.destroy }

class RuboCop::Cop::Custom::AvoidSymbolBlockPass < RuboCop::Cop::Base
  MSG = "Symbol#to_proc (`&:to_s`) の使用を避け、代わりに `it` などの使用を検討してください。"

  def on_block_pass(node)
    return unless node.children.first&.sym_type?

    add_offense(node)
  end
end

まとめ

  • Ruby には複数のブロック記法が存在します。その歴史的な経緯と使い分けを解説しました。
  • Symbol#to_proc(例:&:to_s)は短く書ける反面、初心者や IDE に優しくない側面があります。
  • より明示的かつ理解しやすい記法として、it(または _1)の使用を推奨します。
  • チーム内でスタイルを統一するために、RuboCop の Custom Cop を使った静的チェックの導入も有効です。

Discussion