Ruby from the ground up

Blocks: code you hand to a method

The idea Ruby is built around — a method calls your block when it is ready — plus the collection toolbox that blocks unlock: sort_by, group_by, inject and tally.

Part 8 of 13

Intermediate19 min readBack to Courses

A block is a piece of code you pass along

You have been using blocks since the loop chapter: everything between { ... } or do ... end is one. A block is not a value you store or a function you name — it is code attached to a method call, and the method decides when to run it: once per item for each, once for the whole list for sort, as many times as it likes for anything it likes.

Two spellings, one idea. Braces bind tightly and suit a single expression; do ... end binds loosely and is the convention for multi-line blocks. There is a real difference in precedence — braces attach to the nearest call — which is why an argument used with a do-end block is often written in parentheses.

Writing a method that takes one

Inside a method, yield runs the block, and it can pass arguments along: yield item. block_given? answers whether a block was attached at all, which is how a method offers one behaviour with a block and a sensible default without. If the method uses yield and no block arrives, Ruby raises — so the guard is politeness, not paranoia.

A block's last expression is its return value, and that matters more than it looks. map uses the block's value as the new item; sort_by uses it as the sort key; select uses it as the decision. The block is not a statement you run for its side effects — it is a small function whose answer the caller consumes.

The toolbox that opens up

sort_by orders by a computed key — sort_by { |order| order[:price] } — and a minus sign reverses a number without a second argument. group_by returns a hash of groups keyed by whatever the block computes, and partition splits into two arrays of yes and no. each_with_object carries an accumulator through the loop with the pair arguments the other way round from inject, which folds the whole list into a single value and is often written inject(:+) when the operation is simply addition.

And tally counts: %w[ruby gem ruby].tally gives { 'ruby' => 2, 'gem' => 1 } in one call — the two-line accumulator from the hash chapter, moved into the language. That progression is the shape of this course: learn the pattern, then learn the method that already knows it.

Blocks remember where they were written

A block is a closure: it carries the local variables that were in scope where it was written. That is why a block can add to a total declared outside it, and why a variable created inside the block is gone when the pass ends. The method receives code plus the little world around it, and calls that code when it is ready.

The return value of the block is what the method consumes, so the last expression matters. map uses it as the new item, select uses it as the decision, and sort_by uses it as the key. A block whose last line is puts returns nil, which is why printing inside a map gives you a list of nil values rather than a list of answers.

The method chooses when to yield

A block is not called by you; it is called by the method you handed it to. each yields once per item, sort_by yields once per item and then sorts the answers, and with_banner in the snippet yields exactly once. That inversion is why the same block syntax can mean so many things.

When a method of your own takes a block, decide the contract: how often it yields, what it passes, and what it does with the answer. Writing that down in the method name or a comment is what makes the block easy to supply.

Three sharp edges. A block variable with the same name as an outer local shadows it, and the outer one keeps its old value. return inside a block written in a method returns from that method, not just the block, which can skip the rest of a loop. And braces bind tighter than do ... end: puts items.map { |x| x } and puts items.map do |x| x end do not parse the same way, so parenthesise the argument when the block is long.

Defining, yielding, and the toolbox

def with_banner
  puts '--- start ---'
  yield
  puts '--- end ---'
end

with_banner do
  puts 'inside the block'
end

def twice
  yield 1
  yield 2
end

twice { |n| puts "called with #{n}" }

def maybe
  return 'no block given' unless block_given?
  yield
end

puts maybe
puts maybe { 'block called' }

orders = [
  { item: 'desk', price: 249.0, qty: 1 },
  { item: 'lamp', price: 24.5, qty: 2 },
  { item: 'mat', price: 19.0, qty: 3 }
]

p orders.map { |o| o[:item] }
p orders.sort_by { |o| o[:price] }.map { |o| o[:item] }
p orders.group_by { |o| o[:price] > 20 ? 'pricey' : 'budget' }

puts orders.sum { |o| o[:price] * o[:qty] }
p [1, 2, 3, 4].inject(:+)

p %w[ruby gem ruby].tally
p %w[one two three].each_with_object({}) { |word, acc| acc[word] = word.length }

A workshop inventory

Run it and read each answer. Then add a block call that groups the parts by a price band, and write your own method that yields each item once with a label of your choosing.

Output
Click "Run" to execute your code

Read a block as a question the method is asking. group_by asks "which group does this belong to", sort_by asks "what is this worth for ordering", select asks "do we keep it". Once the question is clear, the right method is usually obvious too.

Try these next

  • Write a method that yields each item of an array with its index, and call it with a block that prints a label.
  • Group a list of words by length with group_by, then count each group.
  • Sort a list of hashes by two keys: price first, then name.

A report built from blocks

Run it and read each answer. Then add a fourth word, change the grouping rule, and write a method of your own that yields each word and collects the block results in an array.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What does yield do inside a method?