Ruby's Symbol-to-Proc: Understanding .map(&:to_s) Syntax

Ruby's Symbol-to-Proc: Understanding .map(&:to_s) Syntax

In Ruby, you'll often see code like this:

numbers = [1, 2, 3]
strings = numbers.map(&:to_s)
# => ["1", "2", "3"]

This is equivalent to:

strings = numbers.map { |n| n.to_s }

But it's shorter. The &:to_s syntax converts a symbol into a block, calling the to_s method on each element.

How It Works

The & operator, when applied to a symbol in a method argument, does two things:

  1. Calls to_proc on the symbol
  2. Passes the resulting proc as a block to the method

Symbols in Ruby have a to_proc method that returns a proc. That proc calls the method named by the symbol on whatever object it receives:

:to_s.to_proc  # Returns a proc equivalent to: ->(obj) { obj.to_s }

When you write .map(&:to_s), Ruby expands it to:

.map(&:to_s.to_proc)
# Which becomes:
.map { |obj| obj.to_s }

The Symbol#to_proc Method

Under the hood, Symbol#to_proc is defined roughly like this:

class Symbol
  def to_proc
    ->(obj, *args) { obj.send(self, *args) }
  end
end

It creates a lambda that calls send on the object with the symbol as the method name. If you pass :to_s, the lambda calls obj.send(:to_s), which is obj.to_s.

Common Uses

Converting types:

[1, 2, 3].map(&:to_s)    # ["1", "2", "3"]
["1", "2", "3"].map(&:to_i)  # [1, 2, 3]

Calling methods on collections:

words = ["hello", "world"]
words.map(&:upcase)  # ["HELLO", "WORLD"]
words.map(&:length)  # [5, 5]

Filtering with select:

[1, nil, 2, nil, 3].compact  # [1, 2, 3]
# Or using select:
[1, nil, 2, nil, 3].select(&:itself)  # [1, 2, 3]

Checking boolean methods:

strings = ["", "hello", "", "world"]
strings.select(&:empty?)  # ["", ""]
strings.reject(&:empty?)  # ["hello", "world"]

When It Doesn't Work

Symbol-to-proc only works for methods that take no arguments (or have default arguments):

# Works - no arguments
[1, 2, 3].map(&:to_s)

# Doesn't work - requires an argument
[1, 2, 3].map(&:+)  # SyntaxError

For methods needing arguments, use a block:

[1, 2, 3].map { |n| n + 10 }  # [11, 12, 13]

Chaining Method Calls

You can't chain methods with symbol-to-proc directly:

# Doesn't work
words.map(&:upcase.reverse)  # SyntaxError

# Works - use a block
words.map { |w| w.upcase.reverse }

Symbol-to-proc is limited to a single method call per symbol.

Performance Considerations

Symbol-to-proc has slight overhead from calling to_proc and creating the lambda. For tight loops over large collections, explicit blocks might be marginally faster:

# Explicit block - slightly faster
large_array.map { |n| n.to_s }

# Symbol-to-proc - cleaner
large_array.map(&:to_s)

In practice, the difference is negligible. Readability usually wins.

The & Operator in Context

The & operator has multiple uses in Ruby:

Converting symbol to proc:

[1, 2, 3].map(&:to_s)

Converting proc to block:

my_proc = ->(n) { n * 2 }
[1, 2, 3].map(&my_proc)  # [2, 4, 6]

Capturing block as proc:

def my_method(&block)
  block.call  # Call the block as a proc
end

my_method { puts "Hello" }

In method definitions, &block captures the block as a proc. In method calls, &symbol converts the symbol (or proc) to a block.

When to Use Symbol-to-Proc

Use it when:

  • You're calling a single method with no arguments
  • The intent is clear from the symbol name
  • You want concise, idiomatic Ruby

Don't use it when:

  • The method needs arguments
  • You need to chain methods
  • The logic is complex enough that a block is clearer

Alternatives and Variations

Ruby 2.6 introduced composition with >>:

add_one = ->(n) { n + 1 }
double = ->(n) { n * 2 }
composed = add_one >> double

[1, 2, 3].map(&composed)  # [4, 6, 8]

You can also define custom to_proc methods on your own classes:

class Multiplier
  def initialize(factor)
    @factor = factor
  end
  
  def to_proc
    ->(n) { n * @factor }
  end
end

multiplier = Multiplier.new(3)
[1, 2, 3].map(&multiplier)  # [3, 6, 9]

Historical Context

Symbol-to-proc was introduced in Ruby 1.9. Before that, you'd see this pattern implemented in libraries like ActiveSupport:

# Pre-1.9 with ActiveSupport
[1, 2, 3].map(&:to_s)  # Required ActiveSupport

# Ruby 1.9+
[1, 2, 3].map(&:to_s)  # Built into language

It became so popular in Rails that it was adopted into core Ruby.

Related Patterns

Symbol-to-proc is part of Ruby's broader functional programming features:

# Method references
method_ref = :to_s.to_proc
[1, 2, 3].map(&method_ref)

# Partial application with curry
add = ->(a, b) { a + b }
add_five = add.curry.(5)
[1, 2, 3].map(&add_five)  # [6, 7, 8]

Reading Symbol-to-Proc Code

When you see &:method_name, read it as "call method_name on each element":

users.map(&:email)  # "Get the email of each user"
items.select(&:valid?)  # "Select items that are valid"
records.reject(&:empty?)  # "Reject records that are empty"

Further Reading

The Ruby documentation on Symbol#to_proc explains the implementation.

For deeper understanding of procs and lambdas, see the Proc class documentation.

The book Eloquent Ruby by Russ Olsen has a chapter on blocks and procs that covers symbol-to-proc in the context of idiomatic Ruby.

Symbol-to-proc is quintessentially Ruby—concise, expressive, and a little magical.

Wear the code

Product mockup

(1..10).map(&:to_s) Developer T-Shirt (Ruby Edition — Dark Mode)

£25.00

View product
Product mockup

(1..10).map(&:to_s) Developer T-Shirt (Ruby Edition — Light Mode)

£25.00

View product

0 comments

Leave a comment

Please note, comments need to be approved before they are published.