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 (2)

Collapse
 
svyatov profile image
Leonid Svyatov

Nice walk through the bridge, the arity table especially. One thing that fits right here and trips people up: proc and lambda differ on return.

A return inside Proc.new { return 10 } returns from the enclosing method, while the same return in lambda { return 10 } returns only from the lambda and the method keeps running:

def via_proc;   p = Proc.new { return 10 }; p.call; 20; end
def via_lambda; l = lambda   { return 10 }; l.call; 20; end

via_proc   # => 10   (return escaped the whole method)
via_lambda # => 20   (return escaped only the lambda)
Enter fullscreen mode Exit fullscreen mode

Worth a line here, because the moment you capture a block with &blk you're holding a Proc, so it's proc return semantics you've got.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.