In Ruby, it’s common to see the shorthand syntax using symbols as block arguments, such as &:to_s
.
While this looks concise and elegant, it can be confusing for beginners and isn’t always well supported by IDEs or refactoring tools.
Recently, more intuitive options like it
and _1
have been introduced, which raises the question: should teams start unifying their style around them?
This article explores the idea of intentionally avoiding the symbol block-pass syntax and shows a practical approach using RuboCop.
The Evolution of Block Parameters
Ruby has gone through several stages of block parameter evolution.
# All of the following return ["1", "2", "3"]
# The most primitive style
[1, 2, 3].map { |i| i.to_s }
# Since Ruby 1.9
[1, 2, 3].map(&:to_s)
# Since Ruby 2.7 (Numbered Parameters)
[1, 2, 3].map { _1.to_s }
# Since Ruby 3.4 (it parameter)
[1, 2, 3].map { it.to_s }
The primitive style
The classic way requires explicitly naming a block parameter:
[1, 2, 3].map { |i| i.to_s }
Even though the variable name doesn’t matter, you still need to provide one.
Symbol#to_proc style
Ruby 1.9 introduced the shorthand using Symbol#to_proc
:
[1, 2, 3].map(&:to_s)
Internally, :to_s.to_proc
behaves like this:
sym = :to_s
blk = sym.to_proc
# Roughly equivalent to:
# ->(obj, *args, **kwargs, &block) { obj.public_send(:to_s, *args, **kwargs, &block) }
blk.call(1) # => "1"
When passed as a block (&:to_s
), Ruby implicitly calls to_proc
and executes the method.
Numbered Parameters and it
Later came Numbered Parameters (_1
) in Ruby 2.7, and the it
shorthand in Ruby 3.4, both expressing the idea that “the name doesn’t matter.”
[1, 2, 3].map { _1.to_s }
[1, 2, 3].map { it.to_s }
Other languages also have similar constructs, like Kotlin’s it
or Scala’s _
.
Why Avoid Symbol Block-Pass?
The motivation behind these newer syntaxes is to make the “namelessness” of the parameter explicit.
Both _1
and it
solve this issue, and personally, I prefer it
—it reads naturally, it’s the newest addition, and it aligns with current Ruby trends.
The problem with Symbol#to_proc
is that non-Ruby developers often struggle to understand it at a glance.
Ruby is fun because it offers multiple ways to write the same thing, but if we want Ruby to stay beginner-friendly and appealing, readability matters.
Also, _1
and it
integrate better with IDEs and refactoring tools.
That’s why I believe Symbol#to_proc
should be limited to code golf or niche cases, and we should stop using it in everyday production code.
A Custom RuboCop Cop
To enforce this idea, here’s a custom RuboCop cop.
It flags usages like array.map(&:to_s)
and suggests replacing them with it
(or _1
).
It works like the inverse of Style::SymbolProc.
# To enable this custom cop, add the following to `.rubocop.yml`:
#
# require:
# - path/to/custom/cop/avoid_symbol_block_pass.rb
#
# Custom/AvoidSymbolBlockPass:
# Enabled: true
#
# --- Offense (NG) ---
# array.map(&:to_s)
# users.each(&:destroy)
#
# --- Allowed (OK) ---
# array.map { it.to_s }
# users.each { it.destroy }
class RuboCop::Cop::Custom::AvoidSymbolBlockPass < RuboCop::Cop::Base
MSG = "Avoid using Symbol#to_proc (`&:to_s`). Consider using `it` or `_1` instead."
def on_block_pass(node)
return unless node.children.first&.sym_type?
add_offense(node)
end
end
Conclusion
- Ruby provides multiple block syntaxes, each with historical context.
-
Symbol#to_proc
(&:to_s
) is concise, but not beginner-friendly and not IDE-friendly. - Prefer clearer alternatives like
it
(or_1
). - Use a RuboCop custom cop to enforce this style within your team.
By shifting away from the symbol block-pass syntax, we can make Ruby codebases more approachable, consistent, and easier to maintain.
Top comments (0)