/ Tags: RUBY 3 / Categories: RUBY

Symbol#to_proc and Callable Objects in Ruby — Making Code Do the Talking

Ruby has several ways to represent callable behavior — blocks, procs, lambdas, and method references — and the syntactic sugar that connects them is one of the language’s quieter strengths. The &:symbol shorthand you use daily is actually a specific invocation of a broader protocol. Understanding how it works, and what else the protocol enables, opens up a style of Ruby that’s both more expressive and more composable.

How &:symbol Actually Works


The & operator in a method call converts its argument to a block. It does this by calling to_proc on the object. Symbols have a to_proc method: :upcase.to_proc returns a proc that calls upcase on its first argument.

Example:

# These are equivalent
["hello", "world"].map(&:upcase)
["hello", "world"].map { |str| str.upcase }

# What's happening:
symbol_proc = :upcase.to_proc
# => #<Proc:0x... (&:upcase)>
symbol_proc.call("hello")
# => "HELLO"

This works for any method that takes no additional arguments:

Example:

[1, nil, 2, nil, 3].select(&:present?)  # Rails
[1, nil, 2, nil, 3].reject(&:nil?)
["  hello  ", "world"].map(&:strip)
[1, -2, 3, -4].select(&:positive?)

When you need arguments, the shorthand doesn’t work — you need a full block or a lambda.

Method Objects — References to Named Methods


method(:name) captures a reference to a named method as a callable object. You can call it directly or convert it to a block.

Example:

# Capturing a method reference
m = method(:puts)
m.call("hello")        # => prints "hello"
m.("hello")            # same — .() is sugar for .call()
m["hello"]             # same

# Instance methods
user = User.new(name: "Alice")
validator = user.method(:valid?)
validator.call          # => true or false

Example:

# Converting to a block with &
[1, 2, 3].each(&method(:puts))
# Equivalent to: [1, 2, 3].each { |n| puts n }

# Useful with existing methods
def double(n) = n * 2

[1, 2, 3].map(&method(:double))
# => [2, 4, 6]

This is especially useful when you have a method that’s already defined and you want to use it in a pipeline without wrapping it in a redundant lambda.

Proc Composition with >> and <<


Ruby 2.6 added >> and << for composing procs and lambdas into pipelines.

Example:

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

# >> pipes left-to-right (double first, then increment)
double_then_increment = double >> increment
double_then_increment.call(5)   # => 11  (5*2=10, 10+1=11)

# << pipes right-to-left (increment first, then double)
increment_then_double = double << increment
increment_then_double.call(5)   # => 12  (5+1=6, 6*2=12)

Example:

# Building reusable transformation pipelines
normalize  = ->(s) { s.strip.downcase }
slugify    = ->(s) { s.gsub(/\s+/, "-").gsub(/[^a-z0-9-]/, "") }
truncate   = ->(s) { s[0..49] }

to_slug = normalize >> slugify >> truncate

to_slug.call("  Hello, World! -- Ruby 3.2  ")
# => "hello-world----ruby-32"

Composition works with method references too: method(:normalize) >> method(:slugify).

Curry — Partial Application


curry transforms a multi-argument lambda into a series of single-argument functions, each returning the next function until all arguments are satisfied.

Example:

add = ->(a, b) { a + b }

add5 = add.curry.(5)   # partial application — one arg provided, one pending
add5.(3)               # => 8
add5.(10)              # => 15

# More practical: building specialized functions from a general one
multiply = ->(factor, n) { n * factor }
double   = multiply.curry.(2)
triple   = multiply.curry.(3)

[1, 2, 3, 4].map(&double)   # => [2, 4, 6, 8]
[1, 2, 3, 4].map(&triple)   # => [3, 6, 9, 12]

Example:

# Curried validators
in_range = ->(min, max, n) { n.between?(min, max) }
valid_age = in_range.curry.(18).(120)

[15, 25, 200, 45].select { |n| valid_age.(n) }
# => [25, 45]

Currying is valuable when you have a general operation and need several specialized versions of it. It’s not always the clearest choice — a named method or a simple lambda can be more readable. Use it when the pattern genuinely removes duplication.

When to Reach for These Patterns


Use &:symbol freely

It’s idiomatic Ruby and everyone recognizes it. Default to it whenever you’re calling a single no-argument method on each element.

Use &method(:name) when you already have a method

If you’ve already defined a method and want to use it as a block, &method(:name) avoids the wrapper lambda. Particularly useful in pipelines and when the method is doing real work.

Use composition for reusable pipelines

When you find yourself chaining the same sequence of transformations in multiple places, composing them into a named pipeline object is cleaner than duplicating the chain.

Use curry sparingly

Currying has a learning curve. If the person reading the code six months from now would have to stop and think about what it does, a named method or a straightforward lambda is probably clearer.

Pro-Tip: method(:pp) is surprisingly useful inside tap chains for debugging. array.tap(&method(:pp)).map { |x| transform(x) } prints the array at that point in the chain without interrupting it. When you’re done debugging, delete the tap call — the rest of the chain is unchanged. It’s cleaner than inserting a puts and a binding.pry and then removing them both.

Conclusion


Ruby’s callable objects — Symbol#to_proc, method references, proc composition, and curry — form a coherent system for building expressive, reusable transformations. The &:symbol shorthand you use every day is the entry point; method objects and proc composition extend the same idea to named methods and multi-step pipelines. None of these require a functional programming background to use well. They’re pragmatic tools for writing Ruby that reads closer to the problem it’s solving than to the implementation details of how it’s solving it.

FAQs


Q1: What’s the difference between a proc and a lambda in Ruby?
Two main differences: lambdas check arity (argument count) strictly and raise ArgumentError if wrong; procs are lenient. Lambdas use return to return from the lambda itself; procs use return to return from the enclosing method. For most callable use cases, use lambdas (->) — the stricter behavior catches bugs earlier.

Q2: Can I use &:method_name with methods that take arguments?
No — &:method_name creates a proc that calls the method with no additional arguments beyond the receiver. For methods with arguments, use a full block or a curried lambda. [1, 2, 3].map(&:+(10)) doesn’t work; use [1, 2, 3].map { |n| n + 10 } or [1, 2, 3].map(&method(:add).curry.(10)).

Q3: Is method(:name) expensive to call?
It allocates a Method object, which is a small allocation. In tight loops called millions of times, this can matter. For most code — pipelines, data transformations, API handlers — the allocation is negligible. Benchmark if you’re unsure; optimize if measurements confirm it’s a bottleneck.

Q4: When should I use >> composition vs just chaining map/select calls?
Chained iterators are usually clearer for single-pass transformations: array.map(&:strip).reject(&:empty?). Proc composition is more useful when the transformation itself is a reusable unit you want to name, pass around, or apply in different contexts. If the pipeline only appears once, keep it as chained calls.

Q5: How does to_proc relate to duck typing?
Any object that responds to to_proc can be converted to a block with &. Symbols, Method objects, and Procs all implement to_proc. You can add to_proc to your own classes to make them usable with & in any method that yields to a block — a useful hook when building DSLs or fluent APIs.

cdrrazan

Rajan Bhattarai

Full Stack Software Developer! 💻 🏡 Grad. Student, MCS. 🎓 Class of '23. GitKraken Ambassador 🇳🇵 2021/22. Works with Ruby / Rails. Photography when no coding. Also tweets a lot at TW / @cdrrazan!

Read More