Dwi Wahyudi
Senior Software Engineer (Ruby, Golang, Java)
Overview
So recently I have a question from another dev regarding Ruby block, on how to read it. For some developers, it can be quite confusing at first, but Ruby block is just a closure (similar to closure in JavaScript). With that closure we “package” the function and all the variables inside as a section of code that can be passed as parameter to other functions. That section of code (which is a block or proc in Ruby) is THE closure.
In this Ruby code: [1, 2, 3].each { |num| puts num }
.
{ |num| puts num }
here is THE block.
We can store it as variable with a proc, a_proc = Proc.new({ |num| puts num })
By coincidence, recently, in Golang community, some developers are angry about iterator pattern, that will be introduced in Go 1.23 https://www.gingerbill.org/article/2024/06/17/go-iterator-design/
I recently saw a post on Twitter showing the upcoming Go iterator design for Go 1.23 (August 2024). From what I can gather, many people seem to dislike the design.
I can understand them, introducing a new concept can be tricky for some people, especially now we can do many ways in order to do one thing. Now Golang is a functional programming language as well.
Will you do such iteration with imperative way or functional way? Your choice.
Practical Block Example
Anyway, so I quickly searched for “Custom ruby iterator”, and found this article: https://clouddevs.com/ruby/create-custom-iterator/. Iterator pattern usually follows functional programming paradigm with block / first class function as its foundation.
The explanation and the example are short and clear, except for the class name, which I rename to SquareIterator
.
class SquareIterator
def initialize(numbers)
@numbers = numbers
end
def iterate
index = 0
while index < @numbers.length
yield @numbers[index] * @numbers[index]
index += 1
end
end
end
numbers = [1, 2, 3, 4, 5]
iterator = SquareIterator.new(numbers)
iterator.iterate do |squared|
puts "Squared value: #{squared}"
end
The output will be like this:
Squared value: 1
Squared value: 4
Squared value: 9
Squared value: 16
Squared value: 25
Well, this code section is the block/closure:
do |squared|
puts "Squared value: #{squared}"
end
For people who are used to imperative code (Golang developers for example), this piece of code may be confusing and hard to understand. We’ll talk about in a moment. If you’re a Ruby developer and is still confused by how block works, skip until Closure topic below.
That yield
call is quite confounding yet mysterious for people who first encounters it. In Ruby, block is just a piece of function body.
We can call iterate like this: iterator.iterate { |squared| puts "Squared value: #{squared}" }
But we cannot do it like this: iterator.iterate({ |squared| puts "Squared value: #{squared}" })
Because block is NOT a variable. It’s a block… and it’s not an object in Ruby.
We can visually imagine this block operation as follows:
- In the block example, imagine this
puts "Squared value: #{squared}"
block body to replace theyield
place there. - Then, imagine the
squared
block param there as the place foryield
param. yield
param@numbers[index] * @numbers[index]
will take place thesquared
block param.- Because the
yield
is inside a loop, repeat this operation multiple times as well. So we get repeatedputs
into the console.
def iterate
index = 0
while index < @numbers.length
# yield @numbers[index] * @numbers[index]
puts "Squared value: #{@numbers[index] * @numbers[index]}"
index += 1
end
end
# ...
iterator.iterate
# iterator.iterate do |squared|
# puts "Squared value: #{squared}"
# end
So, put the block body to replace the yield, use yield params to replace the block params.
If there are more than 1 params, it would be like this:
yield(a, b, c, d, e)
example_method do |a, b, c, d, e|
So in relationships with collection of data (array), we can place this block with this kind of thinking:
“For a collection of things (array), what do we want to do? Can we make those iterate function (to square the numbers) reusable?”
If yes, then block can be used. Let say from this iteration, we want to repeat a certain operation.
iterator.iterate do |squared|
repeat_something(squared)
end
This is the goal of “Iteration design pattern”. To reuse the iteration.
Implicit or Explicit Block
What if there’s no block, and we just call it like iterator.iterate
only. It will raise error, because yield will raise error if no block is given. We can make sure that this code won’t be broken if such condition happened.
We can check if implicit block is given by checking block_given?
condition. Like this:
yield @numbers[index] * @numbers[index] if block_given?
We won’t yield anything to the caller if no block is given. It will just iterate with this condition while index < @numbers.length
and doing nothing.
Previous examples used implicit block, there’s no block argument in iterate
method. We can make it explicit by adding block parameter like this:
class SquareIterator
def initialize(numbers)
@numbers = numbers
end
def iterate(&block)
index = 0
while index < @numbers.length
block.call(@numbers[index] * @numbers[index]) # Call to explicit block.
index += 1
end
end
end
# The rest of code is still the same.
numbers = [1, 2, 3, 4, 5]
iterator = SquareIterator.new(numbers)
iterator.iterate do |squared|
puts "Squared value: #{squared}"
end
Anyway, with this block being implicit or not, how does block and all of this functional programming even work?
Well the reason for us to discuss explicit block, is that in my opinion it is easier to explain about closure and functional programming as a whole by using explicit block.
Closure
Let’s add some changes, we want to store the squared values to an array.
class SquareIterator
def initialize(numbers)
@numbers = numbers
end
def iterate(&block)
index = 0
while index < @numbers.length
block.call(@numbers[index] * @numbers[index]) # Call to explicit block.
index += 1
end
end
end
numbers = [1, 2, 3, 4, 5]
iterator = SquareIterator.new(numbers)
# new array to store squared numbers, will be part of the block/closure.
squared_numbers = []
iterator.iterate do |squared|
squared_numbers << squared
puts "Squared value: #{squared}"
end
puts squared_numbers
Variable squared_numbers
will be [1,4,9,16,25]
.
But wait, we understand that block body will be passed as block into the iterate
method. How does iterate
method know about squared_numbers
? squared_numbers
doesn’t belong to SquareIterator
or iterate
scope.
This is how closure works. Ruby compiler/interpreter will understand that squared_numbers
belongs to the block/closure and refers to the squared_numbers
declaration outside the SquareIterator
class. Ruby compiler while in iterate
method, knows the variable of squared_numbers
declared from outside iterate
or SquareIterator
scope and able to update the variable as well.
Many other programming languages support Closure, for example JS and Golang.
https://en.wikipedia.org/wiki/Closure_(computer_programming)
Proc and Lambda Example
Let’s assign such block to a variable, with a proc, like this:
class SquareIterator
def initialize(numbers)
@numbers = numbers
end
def iterate(a_proc)
index = 0
while index < @numbers.length
a_proc.call @numbers[index] * @numbers[index]
index += 1
end
end
end
numbers = [1, 2, 3, 4, 5]
iterator = SquareIterator.new(numbers)
a_proc = Proc.new { |squared| puts "Squared value: #{squared}" }
iterator.iterate(a_proc)
We just need to change yield
to a_proc.call
.
It’s almost identical. Call to block and proc is slower and explicit. I personally prefer it over the implicit yield
.
And there’s another thing called lambda.
lambda = lambda { |squared| puts "Squared value: #{squared}" }
lambda = -> (squared) { puts "Squared value: #{squared}" } # a shorthand syntax
iterator.iterate(lambda)
It’s almost the same thing with proc.
-
Lambda checks the arguments number (wrong arguments number will raise error), while proc doesn’t (will just assign missing arguments as
nil
) and -
when a lambda returns, return value and execution flow goes back to the calling method while proc doesn’t (will just return immediately).
def lambda_example a_lambda = lambda { return "Inside lambda" } a_lambda.call "Outside Lambda" end puts lambda_example puts '===========' def proc_example a_proc = Proc.new { return "Inside proc" } a_proc.call "Outside proc" end puts proc_example
The output will be like this:
Outside Lambda =========== Inside proc
Should we use proc or lambda? I personally prefer lambda because it checks the arguments number strictly.