Ruby blocks made easy, part III ~grand finale~, blocks and syntactic sugar

Ruby blocks made easy, part III ~grand finale~, blocks and syntactic sugar

Understanding the fundamentals of Ruby blocks

In this series of posts, we already covered that methods can be transformed into procs and as such, can be evaluated later. Furthermore, we've seen that procs can be used as arguments to another methods and that such procs can optionally use curried arguments.

Until now, we have been using methods as a way to represent "blocks" of code:

def multiply(a, b)
  a * b

Also, we learned that, in order to evaluate a method later, we must transform it into a proc (method(:some_method)). In Ruby, we can represent blocks of code to be evaluated later not only in methods, but also creating procs directly:

current_time = { } # => 2021-04-10 17:22:06

# It's quite similar to using methods
def current_time

method(:current_time).call # => 2021-04-10 17:22:10

Then, blocks can represent any group of code which will be evaluated later. Blocks can be inline or multiline:

# inline block { }

# multine block do

Let's take our example in the previous post about map_numbers and, instead of creating a method multiply, we define a Proc directly with a block:

multiply = { |a, b| a * b }, 3) # => 6
multiply.curry[2].call(4) # => 8

Right. Now, remember that the implementation of map_numbers takes a proc as the last argument? Then we have nothing to do in that method. It will simply work, because the object passed as argument should respond to a method call, so in this case procs already do!

multiply = { |a, b| a * b } 

map_numbers([1, 2, 3], multiply.curry[2]) # => [2, 4, 6]

We could also use another way of creating a proc, which is a lambda. There are slight differences between procs and lambdas, but both belong to the same Ruby class: Proc, with lambda being a "type" of Proc.

multiply_proc = { |a, b| a * b }, 3) # => 6

multiply_lambda = -> (a,b) { a * b }, 3) # => 6

# let's bring methods into play
def multiply(a, b)
  a * b

method(:multiply).call(2, 3) # => 6

"Meta" methods, procs, lambdas...they have little differences in practice but they all:

  • take blocks
  • respond to .call
  • respond to .curry
  • and share other similarities... Take a look at the Proc and Method documentation.

YAY! That's so much power!

A syntactic sugar

Our method map_numbers looks like this:

def map_numbers(numbers, calculation_proc)
  # logic here
  #  somewhere, it does ``

The standard Ruby gives us a syntactic sugar, a keyword called yield, which is as similar as calling If we choose to use yield, we can omit the proc parameter but we have to trust that whoever calls the method, they must ensure the proc was passed as the last argument.

def map_numbers(numbers)
  new_list = []
  for number in numbers
    new_list << yield(number) # <--- similar as doing the proc call

Now, if we try to call:

map_numbers([1, 2, 3], method(:multiply).curry[2])


ArgumentError (wrong number of arguments (given 2, expected 1))

That's because, this syntactic sugar has a rule of thumb: the argument cannot be a proc, but a BLOCK instead. For doing so, we have to transform our proc into a block, upon the passing argument, by prepending a & in the proc object:

map_numbers([1, 2, 3], &method(:multiply).curry[2]) # => [2, 4, 6]

The & prepend can be used to transform procs into blocks ONLY upon methods passing arguments!

Passing blocks to methods

Similar as using blocks to define procs and lambdas, we can also use blocks to be passed to methods. In case there's a block being passed, Ruby WILL always take the block and use it as the last argument.

map_numbers([1, 2, 3]) { |number| number * 2 } # => [2, 4, 6]
map_numbers([1, 2, 3]) { |number| number * 3 } # ...
map_numbers([1, 2, 3]) { |number| number + 55 } # ...

# multiline
map_numbers([1, 2, 3]) do |number|
  number * 10

Thankfully, we don't need to create such a method map_numbers in our codebase. Ruby has a lot of useful methods in its standard library, and the method map is one of them, being part of the Array class:

[1, 2, 3].map { |number| number * 2 } # => [2, 4, 6]

And, since we know that methods can be transformed into procs:

multiply_by_two = 2.method(:*) # 2 is an object, don't forget!

[1, 2, 3].map(&multiply_by_two)

So, unleash the madness and abuse on the syntactic sugar!

[1, 2, 3].map(&2.method(:*)) # multiply by 2
[1, 2, 3].map(&6.method(:+)) # sum by 6

Reducing structures

What if we wanted to sum all numbers in a list? Well, that's a simple algorithm:

def sum_all(numbers)
  sum = 0
  for number in numbers
    sum += number

But how can we write a more flexible and robust code that allows to apply any transformation, reducing the entire list into a single accumulated value, no matter if the desired output is a sum or the product of multiplication?

Yes, we can rely on blocks!

def reduce(numbers, initial_acc)
  accumulator = initial_acc
  for number in numbers
    accumulator = yield(accumulator, number)

Then, we can use our method to apply a bunch of reducers:

# sum all numbers
reduce([1, 2, 3], 0) { |acc, number| acc = acc + number }

# multiply all numbers
reduce([1, 2, 3], 1) { |acc, number| acc = acc * number }

# syntactic sugar
reduce([1, 2, 3], 0, &:+) # sum all numbers
reduce([1, 2, 3], 1, &:*) # multiply all numbers

Similar to map, Ruby also provides a method reduce in the standard library:

[1, 2, 3].reduce(0) { |acc, number| acc += number }
[1, 2, 3].reduce(1) { |acc, number| acc *= number }

[1, 2, 3].reduce(&:+)
[1, 2, 3].reduce(&:*)

Wrapping up

In this series of blogposts, we tried to cover the fundamentals behind Ruby blocks, such as:

  • how Ruby evaluates expressions
  • how we can use methods to evaluate expressions later
  • methods and procs
  • procs as arguments
  • curry arguments in procs
  • blocks in procs, lambdas and methods
  • bonus point to syntactic sugar and the Ruby standard library

I hope you could enjoy and understand a bit more on how Ruby blocks work and how to make a more effective use of them on a daily basis!

Share this