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¶
- Write
repeat(n)that yields each index from0ton - 1and returns the array of the block's values. When no block is given, raise anArgumentErrorwhose message names the call, instead of lettingyieldraiseLocalJumpError. Call it with{ }and withdo…end. - Write
twice(&block)that callsblock.calltwo times and returns both values. Printblock.lambda?and whatblockis when the caller attaches nothing, then maketwicereturn anEnumeratorin that case withreturn 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.
See also¶
- Procs and lambdas differ in arity and
return— the object an&blockparameter holds, and why it is a proc rather than a lambda next,breakandreturnleave a block differently — the three ways out of a block- Closures capture variables, not values — what a block sees of the method that wrote it
- Arguments: positional, keyword and splat —
&blkbeside*restand**optsin a parameter list - Parentheses are optional — why
show [1, 2].map do … endparses the way row 8 shows - Destructuring assignment — how an array yielded to
|k, v|splats into block parameters - Building a DSL — blocks as the body of a configuration language
- Gotchas for Python programmers — the
puts [1].map do … endtrap in its list - Rust: a block is an expression ↗ — the word "block" names a scoped expression there, not a callback
- Rust: what a closure is ↗ — Rust's callback is always a value, like Python's
itand_1name the block's parameter — A block with no|x|can read its arguments as_1,_2, … (Ruby 2.7) or asit(Ruby 3.4); they are…&:symcallsSymbol#to_proc—&xin an argument list callsx.to_procand attaches the result as the block; a symbol'sto_procis a…- Common error messages — Twenty-nine errors you will meet in your first month of Ruby, each with the code that raises it, the…
- Mutex guards, Queue hands over, ConditionVariable waits —
Mutex#synchronizemakescount += 1exact across ten threads, aQueuemoves work from one thread to… File.openwith a block closes the file for you —File.open(name, mode) { |f| … }closes the file when the block ends and evaluates to the block's value;…- Indentation is convention in Ruby, syntax in Python — Ruby runs mis-indented code and
ruby -wonly warns when anendsits under the wrong column, while… - There is more than one way, and one obvious way — Ruby's idioms