Skip to content
academia.sh

Lesson 08 / 12

Blocks

The same map message is measured giving two different results with two different blocks, a method is shown calling its block with yield, and the block is shown deciding the result without changing the class's return rule.

Published Updated

In this lesson

  • Shows that a method calls the block given to it with yield
  • Queries whether a block is present with block_given? and defined?(yield)
  • Measures that the same map message gives two results with two blocks
  • Measures that a block's result does not change the return value of each
Contents

The previous lesson showed that Hash.new { 0 }[:x] and Hash.new { |_, k| k.to_s }[:x] give two different results, and that the difference is decided not by the object receiving the message but by the block supplied. In that lesson the block itself was only written. This lesson makes it the subject: a block is a piece of code written with curly braces or do ... end and given to a method together with a message. A block is not a value by itself; it runs when the method calls it. M10/K03 measured how many times a closure runs in PHP; that question is not asked here, and the question is again who answers.

The course’s responders are class, module, block and syntax. This lesson’s responder is the block, but the bounding measurement will show that the block does not decide every result.

The Same Message, Two Blocks

The measurement below gives the same map message on the same array to two different blocks. The array does not change and the message does not change; only the block does. The core classifies the same result too: reply.rb counts the responder as a block when the two results differ.

# reply.rb — helpers that classify who answers a message

# Classification asks in order: is the expression a message at all (defined?),
# then do two different blocks give two different results, then who owns the method.
def responder(definition, owner: nil, result: nil, with_blocks: nil)
  return ["syntax", "defined? #{definition}"] unless definition == "method"

  if with_blocks
    first, second = with_blocks.call
    return ["block", "#{first.inspect} / #{second.inspect}"] unless first == second
  end

  kind = owner.instance_of?(Class) ? "class" : "module"
  [kind, "#{owner}, result #{result.inspect}"]
end
# block.rb — the same map message with two blocks
require_relative "reply"

numbers = [1, 2, 3]
doubled = numbers.map { |n| n * 2 }
text = numbers.map { |n| n.to_s }

kind, evidence = responder(defined?(numbers.map), with_blocks: -> { [doubled, text] })
puts "#{kind}  #{evidence}"
puts numbers.inspect
block  [2, 4, 6] / ["1", "2", "3"]
[1, 2, 3]

The first line is the core’s element mapping observation: the responder is a block. The second line shows that the array did not change; map built a new array. CB3. The measurement is on an array; on a hash, map comes from Enumerable and works with the same block logic, but this lesson does not measure it separately.

How a Method Calls the Block

A method definition can ask whether a block was given when it was called. block_given? says this directly; inside the method body, defined?(yield) gives the same information: a string on a call with a block, nil on a call without one. yield calls the given block and passes its arguments to the block’s parameters. yield is not written at the top level, outside a method; every measurement is inside a method.

# yield.rb — a method calls its block with yield
def twice
  puts "with_block=#{block_given?} yield=#{defined?(yield).inspect}"
  return unless block_given?

  yield 1
  yield 2
end

twice
twice { |n| puts "block #{n}" }
with_block=false yield=nil
with_block=true yield="yield"
block 1
block 2

On the call without a block, block_given? returned false, defined?(yield) returned nil, and the method exited early. On the call with a block, they were true and "yield"; the block ran twice, with 1 and 2. CB4. defined?(yield) returns "yield", not "method"; yield is not a method call but a construct of the language itself.

map and each called without a block raise no error; they return an iterator (Enumerator). On a call with a block, defined? gives "expression"; the core queries the message without a block for this reason, as seen before.

# without_block.rb — a call without a block returns an iterator
puts [1, 2, 3].map.class
puts [1, 2, 3].each.class
puts defined?([1, 2, 3].map)
puts defined?([1, 2, 3].map { |n| n })
Enumerator
Enumerator
method
expression

The Block’s Value and Its Surroundings

yield is an expression and returns the value of the block’s last expression. The method can use this value; map does exactly that. A block also sees the local variables of the place where it was defined: this is the closure behavior explained in M01/K02, and it is not redefined here, only measured.

# value.rb — yield returns the block's value, the block sees its surroundings
def triple
  result = yield 3
  "yield returned: #{result}"
end

factor = 5
puts triple { |n| n * factor }
factor = 7
puts triple { |n| n * factor }

total = 0
[1, 2, 3].each { |n| total += n }
puts "total=#{total}"
yield returned: 15
yield returned: 21
total=6

The block was written the same way in both calls, but it saw the current value of the factor variable: first 15, then 21. The block did not copy the value when it was defined; it read the variable’s current value at the moment it was called. Inside the each block, total increased the outer variable. CB6. This lesson only measures that the block sees the variables of the scope where it was defined; it does not measure other scope rules.

Two Forms, Many Parameters

Curly braces are preferred for one-line blocks and do ... end for multi-line blocks; both build the same block. A block can take several parameters, and the parameters are bound in order to however many values yield gives.

# forms.rb — two forms build the same block, several parameters
def pair
  yield 1, 2
end

short = pair { |a, b| a + b }
long = pair do |a, b|
  a + b
end
puts "short=#{short} long=#{long}"

{ name: "Alice", age: 30 }.each do |key, value|
  puts "#{key}=#{value}"
end
short=3 long=3
name=Alice
age=30

Both forms gave 3. The hash’s each block received the key and the value as two separate parameters. CB7. The two forms were measured to give the same result only in these calls; which method a block binds to in a call without parentheses is measured separately in the Which Method It Binds To section.

Shorthand Forms

Since Ruby 3.4, it can be written in a block with a single parameter; _1 has existed since Ruby 2.7. Both give the same result.

# shorthand.rb — it and _1 in a single-parameter block
puts [1, 2].map { it * 3 }.inspect
puts [1, 2].map { _1 * 3 }.inspect
[3, 6]
[3, 6]

Unpacking Parameters and Order

If a block takes several parameters, a single array passed by the method is distributed across the parameters. |a, b| unpacks a pair, and |a, (b, c)| unpacks a nested array too. each_with_index passes the position along with the element.

# unpacking.rb — array unpacking in block parameters
[[1, 2], [3, 4]].each { |a, b| puts "#{a}+#{b}=#{a + b}" }
%w[x y].each_with_index { |letter, index| puts "#{index}:#{letter}" }
[[1, [2, 3]]].each { |a, (b, c)| puts "a=#{a} b=#{b} c=#{c}" }
puts [[1, 2], [3, 4]].map { |pair| pair }.inspect
1+2=3
3+4=7
0:x
1:y
a=1 b=2 c=3
[[1, 2], [3, 4]]

On [[1, 2], [3, 4]], |a, b| split each pair into two parameters: 1+2=3 and 3+4=7. each_with_index gave the element and the position as two separate parameters. The parenthesized (b, c) unpacked the inner array. The single-parameter map { |pair| pair }, on the other hand, took the array whole, without splitting it. CB23. Unpacking was measured only on array elements and in these four calls.

The Block’s Outer Local

A block sees the local variables of the place where it was defined and can change them. This is the closure behavior measured above. What happens when a block parameter has the same name as an outer variable is also measured below; the |x; y| form makes the name after the semicolon local to the block.

# outer_local.rb — a block sees and changes an outer variable
total = 0
[1, 2, 3].each { |n| total += n }
puts total
n = 100
[1, 2].each { |n| n }
puts n
y = 1
[5].each { |x; y| y = x }
puts y
6
100
1

total was defined outside the block and the block increased it on every turn: the result is 6. In the second example, the block parameter n had the same name as the outer n; the n inside the block became a separate variable and the outer n stayed 100. In the third, |x; y| declared a y local to the block; even though the block wrote y = x, the outer y stayed 1. A block can change an outer local; this happens only when the name is not declared local to the block. CB24.

Which Method It Binds To

The two block forms gave the same result at the start of this lesson. The difference appears in a call without parentheses: curly braces bind to the nearest method, while do and end bound to the outer method in the measurement below. The ask method below only reports whether a block was given to it.

# binding.rb — which method the block binds to
def ask(*) = "block=#{block_given?}"
puts ask(1) { }
puts ask 1 do end
block=true
block=false

ask(1) { } gave the block to ask and block=true was printed. puts ask 1 do end bound the block to puts; ask received no block and block=false was printed. In a call without parentheses, do and end carry the block to the outer method. CB25. Only these two calls were measured; binding in chained calls was not measured separately.

Returning a Value from a Block

Inside a block, next ends the block for that turn and makes the given value the block’s value. break ends the method itself, and the given value becomes the result of the method call.

# next_break.rb — values with next and break
puts [1, 2, 3, 4].map { |n| next 0 if n.even?; n * 10 }.inspect
puts [1, 2, 3].each { |n| break n * 5 if n == 2 }
[10, 0, 30, 0]
10

next 0 if n.even? made the block’s value 0 on even numbers, and map returned [10, 0, 30, 0]. Inside each, break n * 5 cut the loop when n == 2, and the value of the each call became 10. CB26. next and break were measured only with map and each.

A Flexible Number of Parameters

A block does not require the number of parameters to equal the number of values the method passes. Extra values are dropped, and a missing parameter becomes nil. When a single array is passed, several parameters unpack it.

# flexible.rb — block parameter count versus passed value count
def two
  yield 1, 2
end
two { |a| puts "a=#{a.inspect}" }
two { |a, b, c| puts "c=#{c.inspect}" }
def one
  yield [1, 2]
end
one { |a, b| puts "a=#{a} b=#{b}" }
one { |a| puts "a=#{a.inspect}" }
a=1
c=nil
a=1 b=2
a=[1, 2]

yield 1, 2 passed two values. The single-parameter block took only the first, the third parameter of the three-parameter block stayed nil, and no error occurred. yield [1, 2] passed a single array: the two-parameter block unpacked it, and the single-parameter block took the array whole. This looseness is seen in a block and in a proc; the Procs and Lambdas lesson runs the same test with a lambda. CB39.

Using the Block’s Value in a Method

yield is an expression and its value is the block’s last expression. The method can accumulate this value or use it as a condition; in that case, what is done is decided not by the method but by the given block.

# yield_value.rb — the value of a yield expression
def transform(list)
  result = []
  list.each { |x| result << yield(x) }
  result
end
puts transform([1, 2, 3]) { |x| x * 2 }.inspect
def count_matching(list) = list.count { |x| yield x }
puts count_matching([1, 2, 3, 4]) { |x| x.even? }
[2, 4, 6]
2

transform appended each yield(x) value to the result and gave [2, 4, 6]. count_matching counted the elements for which the block returned true and gave 2. In both methods the block decided the result; transform does not know about doubling and count_matching does not know about picking even numbers; both just use the block’s value. The responder is therefore the block. CB40.

Block Parameter and Symbol

If a method defines an &block parameter, it receives the given block as an object; if no block was given, this parameter is nil. The &:name form passes a symbol in place of a block.

# block_object.rb — the &block parameter and a symbol
def take(&b) = b
puts take.inspect
puts take { }.class
puts %w[a b].map(&:upcase).inspect
puts :upcase.to_proc.call("x")
nil
Proc
["A", "B"]
X

take returned nil when called without a block and a Proc when called with one. map(&:upcase) sent the upcase message to each element and gave ["A", "B"]; :upcase.to_proc.call("x") returned X. A symbol itself is not a block; it turns into a Proc with to_proc. CB41. Proc objects are detailed in the Procs and Lambdas lesson.

The Bounding Measurement

Does the block always decide the result? The each message runs the block for every element, but its return value is not the block’s result. Two different blocks give the same return value, and the responder is not the block but the class that owns each.

# return_value.rb — a block's result does not change the return of each
numbers = [1, 2]
a = numbers.each { |n| n * 10 }
b = numbers.each { |n| n * 99 }
puts a.inspect
puts b.inspect
puts "owner=#{numbers.method(:each).owner}"
[1, 2]
[1, 2]
owner=Array

The two blocks computed different values, but both returned [1, 2] from each. The return value is decided not by the blocks but by the definition of each in the Array class. So the block decides the result for map and does not for each; the same block syntax does two different jobs on two messages. CB5. This difference was measured only on these two methods and is not generalized to other iteration methods.

Summary

  • A block is a piece of code given to a method together with a message; the method calls it with yield.
  • This lesson’s responder is the block: the same map message gave [2, 4, 6] with { |n| n * 2 } and ["1", "2", "3"] with { |n| n.to_s }.
  • block_given? and defined?(yield) query whether a block is present; map and each without a block return an iterator.
  • it is available in a single-parameter block since Ruby 3.4, and _1 since Ruby 2.7.
  • The bounding measurement: each returns [1, 2] with both of two different blocks; in this case the Array class decides the return value.

Next Step

map was Array’s method on an array and Enumerable’s method on a hash. The next lesson looks at that module itself: where does a method that the array does not have itself come from, and how do you give your own class the same ability with a single each.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close