Skip to content

Blocks are not objects

Level: 101 · a Python programmer meeting yield and do…end for the first time

One line: A block is syntax attached to one method call — yield runs it, block_given? asks whether it came, and only an &block parameter turns it into an object — so it cannot be assigned, cannot exist on its own, and a call carries at most one.

Every Ruby method call can carry one block: the { |x| … } or do |x| … end written after the arguments. Inside the method, yield runs the block with whatever values yield names and evaluates to the block's last expression (row 1); block_given? reports whether a block came (row 2); a yield that finds no block raises LocalJumpError (row 3). What separates a block from a Python callback is that it is not a value. x = { |a| a } is a SyntaxError, because a block only exists as part of a call (row 4), and a call cannot carry two of them (row 5). The one door from syntax into object is an &block parameter: the method receives a Proc — a plain proc, lambda? false — that it can store, call, and pass on to another method with &blk; when no block was given that parameter is nil (rows 6 and 7). The two spellings differ in precedence: { } binds to the nearest method call and do…end to the outermost one, so show [1, 2].map do … end hands the block to show and map runs without one, returning an Enumerator (rows 8 and 9). Row 10 is the idiom Python spells with: a method that prints before and after its yield and returns whatever the block produced.

Verified output of blocks_are_not_objects_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

 1. yield hands 21 to the block and takes its value: 42
 2. block_given? without a block / with one:       block_given? false / block_given? true
 3. yield with no block:                           LocalJumpError: no block given (yield)
 4. a block on its own, x = { |a| a }:             SyntaxError - a block is not an expression
 5. two blocks on one call, both(&b) { }:          SyntaxError - one block per call
 6. &block reifies it:                             Proc, lambda? false, block.call(1) = 2; no block: nil
 7. passing it on with &blk:                       50
 8. do...end binds to show, not to map:            show received #<Enumerator: [1, 2]:map>, block_given? true
 9. { } binds to map:                              show received [2, 4], block_given? false
10. a block wraps before/after around its body:
    before
    inside
    after
    bracket returned :done

Reading the output

Rows 1 to 3 are the whole protocol: yield 21 sends a value out, the block's x * 2 comes back as the value of yield, block_given? is false until a block is attached, and bare with no block dies with LocalJumpError: no block given (yield). Rows 4 and 5 are compiled from strings with RubyVM::InstructionSequence.compile inside a rescue, and only the exception class is printed: a block on its own and two blocks on one call are both refused before anything runs. Row 6 shows the reified block: an &block parameter is a Proc whose lambda? is false, it answers call(1) with 2, and the same parameter reads as nil when the caller attached nothing. Row 7 threads one block through two methods with &blk. Rows 8 and 9 print what show received: with do…end it received #<Enumerator: [1, 2]:map> — map got no block, so it returned an enumerator — and block_given? inside show was true; with { } it received [2, 4] and no block. Row 10 prints before, inside, after in order and then the value the block returned, :done, which bracket handed back to its caller.

Compared with Python

Python has no block syntax. Everything a Ruby method would yield to is an ordinary object passed as an argument — a lambda, which is a single expression, an inner def, or a functools.partial — so the two rows where Ruby refuses flip: a lambda on its own is a function (row 4), and a call takes any number of callables (row 5). There is nothing to reify (row 6), a missing callback is a default of None rather than a block_given? question (row 2), and calling it is a TypeError at that moment rather than a LocalJumpError (row 3). Rows 8 and 9 have no precedence trap: map takes its callable as an argument, and a comprehension needs none. The closest thing to a block that wraps its body is the with statement (row 10): a contextlib.contextmanager runs code before and after the suite, but with is a statement and has no value, whereas the Ruby bracket { … } call evaluated to :done. The building blocks are documented under lambda expressions ↗, functools.partial ↗ and the with statement ↗.

Verified output of blocks_are_not_objects_py.py — regenerated by tools/run_examples.py, never hand-typed.

 1. a callable takes 21 and hands back its value:  42
 2. callback given? without one / with one:        callback given? False / callback given? True
 3. calling a missing callback:                    TypeError
 4. a lambda on its own, f = lambda a: a:          function - a lambda is an expression
 5. two callables on one call, both(f, g):         [42, 42] - any number
 6. nothing to reify, it is an object already:     function, callable? True, add_one(1) = 2; not passed: None
 7. passing it on as an argument:                  50
 8. map takes the callable as an argument:         show received [2, 4]
 9. the same with a comprehension:                 show received [2, 4]
10. with runs before/after around its suite:
    before
    inside
    after
    the with statement is a statement: it has no value to return

Try it

  1. Write repeat(n) that yields each index from 0 to n - 1 and returns the array of the block's values. When no block is given, raise an ArgumentError whose message names the call, instead of letting yield raise LocalJumpError. Call it with { } and with do…end.
  2. Write twice(&block) that calls block.call two times and returns both values. Print block.lambda? and what block is when the caller attaches nothing, then make twice return an Enumerator in that case with return to_enum(:twice) unless block — the idiom Writing an iterator builds on.
Solution to 1

blocks_are_not_objects_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.

# Exercise 1: repeat(n) yields 0...n and returns the block's values;
# without a block it raises an ArgumentError that names the problem,
# instead of the LocalJumpError a bare yield would raise.

def repeat(n)
  raise ArgumentError, "repeat(#{n}) needs a block" unless block_given?
  (0...n).map { |i| yield i }
end

p repeat(4) { |i| i * i }

begin
  repeat(4)
rescue ArgumentError => e
  puts "#{e.class}: #{e.message}"
end

# The same call written with do...end: the block still belongs to repeat,
# because there is no other method call on the line for it to bind to.
values = repeat(3) do |i|
  i + 10
end
p values

Verified output of blocks_are_not_objects_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

[0, 1, 4, 9]
ArgumentError: repeat(4) needs a block
[10, 11, 12]

See also