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
end
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 = Proc.new { Time.now }
current_time.call # => 2021-04-10 17:22:06
# It's quite similar to using methods
def current_time
Time.now
end
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
Proc.new { Time.now }
# multine block
Proc.new do
Time.now
end
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 = Proc.new { |a, b| a * b }
multiply.call(2, 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 = Proc.new { |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 = Proc.new { |a, b| a * b }
multiply_proc.call(2, 3) # => 6
multiply_lambda = -> (a,b) { a * b }
multiply_lambda.call(2, 3) # => 6
# let's bring methods into play
def multiply(a, b)
a * b
end
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 `calculation_proc.call(number)`
end
The standard Ruby gives us a syntactic sugar, a keyword called yield, which is as similar as calling some_proc.call
. 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
end
new_list
end
Now, if we try to call:
map_numbers([1, 2, 3], method(:multiply).curry[2])
Oh,oh:
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
end
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
end
end
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)
end
accumulator
end
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!
Top comments (0)