Ruby - Blocks

Ruby on Rails

Posted by Theo Cha on June 13, 2023

“Blocks”

What are blocks?


Blocks are enclosed in a do / end statement or between brackets {}, and they can have multiple arguments. The argument names are defined between two pipe characters.

1
2
3
4
5
6
7
8
9
10
11
block_name {
   statement1
   statement2
   ..........
}

block_name do
   statement1
   statement2
   ..........
end
yield keyword

1
2
3
4
5
6
7
8
9
10
11
12
13
def test
   puts "You are in the method"
   yield
   puts "You are again back to the method"
   yield
end

test {puts "You are in the block"}
# Output
# You are in the method
# You are in the block
# You are again back to the method
# You are in the block

You also can pass parameters with the yield statement. Here is an example −

1
2
3
4
5
6
7
8
9
10
11
def test
   yield 5
   puts "You are in the method test"
   yield 100
end
test {|i| puts "You are in the block #{i}"}

# Output:
# You are in the block 5
# You are in the method test
# You are in the block 100
Implicit vs Explicit Blocks

Blocks can be “explicit” or “implicit”.Explicit means that you give it a name in your parameter list. You can pass an explicit block to another method or save it into a variable to use later.

1
2
3
4
def explicit_block(&block)
  block.call # same as yield
end
explicit_block { puts "Explicit block called" }

Why we use the block?


Using blocks give us closure-like structures. Used properly, they can reduce repetition and even make coding less error-prone. Let’s have a look an example. A client has requested feature is the ability to take all the prices on an order and total them. and we have an array of prices from a database.

1
item_prices = [3, 24, 9]

Now, We need a method that takes such an array, and totals the prices. As in most other languages, we could just loop through the array items, and add them to a variable’s value as we go.

1
2
3
4
5
6
7
8
9
10
11
12
def total(prices)
  amount = 0
  index = 0
  while index < prices.length
    amount += prices[index]
    index += 1
  end
  return amount
end

puts total(item_prices)
# Output: 36

After first implement, the client request a second feature that can process a refund for lost orders. It needs to loop through the invoice prices, and subtract each amount from the customer’s account balance.

1
2
3
4
5
6
7
8
9
10
11
12
def refund(prices)
  amount = 0
  index = 0
  while index < prices.length
    amount -= prices[index]
    index += 1
  end
  amount
end

puts refund(item_prices)
# Output: -36

As you can see that the refund method looks highly similar to total; we’re just working with negative values instead of positive ones. also, the client keep requesting the next feature to show discounted prices. If you look at the above 2 methods, there’s some duplicated code that looping through the array of prices. It’s definitely a violation of the DRY (Don’t Repeat Yourself) principle.

How we could extract the repeated code out into another method, and have total, refund, and call it? something like the below code example.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def do_something_with_every_item(array)

  # Set up total or refund variables here,
  # IF we need them.

  index = 0
  while index < array.length

    # Add to the total OR
    # Add to the refund

    index += 1
  end
end

The next question will be how will you set up the variables you need prior to running the loop? And how will you execute the code you need within the loop?

Use-case for the block

If only we could pass a chunk of code into a method, to be executed while that method runs, our problem would be solved. We could rely on do_something_with_every_item to handle the looping for us, and pass it chunks of code that add all the prices, or subtract them, or calculate a discount. A block is basically a chunk of code that you associate with a method call. While the method runs, it can invoke the block one or more times.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def total(prices)
  amount = 0
  do_something_with_every_item(prices) do |price|
    amount += price
  end
  return amount
end

def refund(prices)
  amount = 0
  do_something_with_every_item(prices) do |price|
    amount -= price
  end
  return amount
end

def do_something_with_every_item(array)
  index = 0
  while index < array.length
    yield array[index]
    index += 1
  end
end

By using the block, we don’t write the looping logic twice and reuse the do_something_with_every_item method.