DEV Community

Figsy
Figsy

Posted on

Blocks, Procs, and the "&" Bridge

&blk in a parameter list turns a block into a Proc; &proc at a call turns a Proc back into a block.

1. A block is syntax, not an object

A block is the { ... } or do ... end you attach to a method call. It is not a value you can hold by itself.

You can't store a block in a variable — Ruby won't even parse it:

b = { |x| x * 2 }   # SyntaxError
# Ruby reads { } here as a hash, not a block.
Enter fullscreen mode Exit fullscreen mode

A method can carry at most one block:

[1, 2, 3].each { |x| puts x }   # fine — exactly one block
# There is no syntax to attach a second block to the same call.
Enter fullscreen mode Exit fullscreen mode

Inside a method you reach the block with yield:

def run
  yield        # runs whatever block was attached
  yield        # you can run it again
end
run { puts "hi" }   # prints: hi hi
Enter fullscreen mode Exit fullscreen mode

A block is not a normal argument. It rides along separately from the things in the parentheses:

def how_many(*args)
  args.size
end
how_many(1, 2) { puts "ignored" }   # => 2   (the block is NOT counted as an argument)
Enter fullscreen mode Exit fullscreen mode

2. A Proc is an object

A Proc is a real object that holds a chunk of code. You can do anything with it that you do with any object.

Store it in a variable:

doubler = proc { |x| x * 2 } # or Proc.new { |x| x * 2 }
doubler.class      # => Proc
Enter fullscreen mode Exit fullscreen mode

Call it (three equivalent ways):

doubler.call(5)    # => 10
doubler.(5)        # => 10   (.() shorthand)
doubler[5]         # => 10   ([] shorthand)
Enter fullscreen mode Exit fullscreen mode

Put it in an array and pass it around:

jobs = [proc { puts "a" }, proc { puts "b" }]
jobs.each { |job| job.call }   # prints: a b
Enter fullscreen mode Exit fullscreen mode

So the difference is simple: a block is loose syntax attached to one call; a Proc is an object you can keep.

3. & is the bridge between them

& converts a block into a Proc, or a Proc into a block. Which direction happens depends only on where you write the &.

3.a) & in the parameter list → block becomes a Proc

When you write def foo(&blk), the & catches the block the caller attached and gives it to you as a Proc object named blk:

def foo(&blk)
  blk.class      # => Proc    (now it's a real object)
  blk.call(10)   # => 20      (call it like any object)
end
foo { |x| x * 2 }   # the { } block arrives as the Proc `blk`
Enter fullscreen mode Exit fullscreen mode

Without & you'd have no name for the block — you could only use yield. With &blk you can store it, inspect it, or pass it on.

def run
  yield               # the ONLY way to reach the block
  # blk             — there's no such variable to refer to
  # @saved = blk    — can't store it
  # blk.arity       — can't inspect it
  # other(&blk)     — can't pass it on
end
run { puts "hi" }     # prints: hi
Enter fullscreen mode Exit fullscreen mode

3.b) & at a call site → Proc becomes a block

When you already have a Proc in a variable and want a method to use it as its block, put & in front:

doubler = ->(x) { x * 2 }
[1, 2, 3].map(&doubler)    # => [2, 4, 6]
Enter fullscreen mode Exit fullscreen mode

Here doubler is lambda

Without the &, it's treated as a normal argument and map won't use it as the block:

[1, 2, 3].map(doubler)     # ArgumentError
# wrong number of arguments (given 1, expected 0)
Enter fullscreen mode Exit fullscreen mode

lambda is a Proc

The arrow ->(x) { ... } just creates a Proc object that happens to be the "strict" kind. You can check:

doubler = ->(x) { x * 2 }
doubler.class     # => Proc
doubler.lambda?   # => true
Enter fullscreen mode Exit fullscreen mode

So when the text says "a Proc in a variable", a lambda counts — it is a Proc. The word Proc (capital P) is the class; both ways of making one produce an object of that class:

a = ->(x) { x * 2 }       # lambda
b = lambda { |x| x * 2 }  # lambda (same as above)
c = proc { |x| x * 2 }    # plain proc
d = Proc.new { |x| x * 2 }# plain proc

[a, b, c, d].map(&:class)    # => [Proc, Proc, Proc, Proc]
[a, b, c, d].map(&:lambda?)  # => [true, true, false, false]
Enter fullscreen mode Exit fullscreen mode

3.c) Both directions in one method

Capture a block as a Proc, then forward it as a block to another method. Same &, opposite jobs, decided purely by position:

def logged(&blk)          # & in params: block → Proc `blk`
  puts "start"
  [1, 2, 3].each(&blk)    # & at call: Proc `blk` → block for each
  puts "end"
end
logged { |n| puts n }     # prints: start 1 2 3 end
Enter fullscreen mode Exit fullscreen mode

4. The to_proc rule

One extra rule explains everything else: at a call site, if the thing after & is not already a Proc, Ruby first calls .to_proc on it, then turns the result into a block.

A Symbol knows how to become a Proc:

:upcase.to_proc.call("hi") # => "HI"

%w[a b c].map(&:upcase) # `&` runs `:upcase.to_proc`, then uses it as the block
%w[a b c].map { |s| s.upcase } # exactly the same thing
# both => ["A", "B", "C"]
Enter fullscreen mode Exit fullscreen mode

So &:upcase is roughly the same as &->(x) { x.upcase }.

A Method object does too (remember: method(:name) returns an object):

method(:puts).class               # => Method
method(:puts).to_proc.call("hi")  # prints: hi

[1, 2, 3].each(&method(:puts))    # `&` runs the Method's `to_proc`, then uses it
[1, 2, 3].each { |x| puts x }     # exactly the same thing
# both print: 1 2 3
Enter fullscreen mode Exit fullscreen mode

Even your own objects work after &, as long as they define to_proc:

class Multiplier
  def initialize(by) = @by = by
  def to_proc = proc { |x| x * @by }
end

triple = Multiplier.new(3)
[1, 2, 3].map(&triple) # => [3, 6, 9]
Enter fullscreen mode Exit fullscreen mode

5. The whole thing in two lines

  • &X at a call means: take X, call .to_proc if it isn't already a Proc, then use it as the block.
  • &blk in a parameter list does the reverse — it catches the block and hands it back as a Proc you can hold.

Top comments (0)