If you already know what are blocks in Ruby you can skip to the "Ruby blocks are not just anonymous functions" section. Otherwise, here's a small tutorial.
Small introduction to Ruby blocks
Ruby methods can accept a block. Here's the simplest example:
def run
yield
end
run do
puts "Hello world!"
end
In the above example run
yields. When we call run
we give it a block (do
... end
) Then the block's contents are executed exactly where yield
was used.
With this, we can define something a bit more useful:
def twice
yield
yield
end
twice do
puts "Hello world!"
end
Because we do yield
twice, the block gets executed... twice!
This general idea of executing something a number of times can also be achieved like this in Ruby:
3.times do
puts "Hello world!"
end
And blocks can receive arguments, so you can also do this:
3.times do |i|
puts "Hello number #{i}!"
end
The above outputs this:
Hello number 0!
Hello number 1!
Hello number 2!
Ruby blocks are not just anonymous functions
At first glance Ruby blocks are similar to a concept that exists in many languages: passing anonymous functions to functions that, in turn, receive functions and execute them.
Let's try defining a times
function in Go:
func times(n int, block func(int)) {
for i := 0; i < n; i++ {
block(i)
}
}
This function takes an integer n
and will invoke the given function named block
that many times.
Here's a full program that uses it:
package main
import "fmt"
func times(n int, block func(int)) {
for i := 0; i < n; i++ {
block(i)
}
}
func main() {
times(3, func(i int) {
fmt.Printf("Hello number %v!\n", i)
})
}
The output is the same as the 3.times
Ruby snippet mentioned before.
Aside from having to write a bit more code, and having a bit more noise in it, it might seem that blocks aren't that special in Ruby after all... you can do the above thing in Go, Java, C#, Rust, and many more.
To show that Ruby blocks are different, I came up with this program:
def greet_20_numbers
seconds = Time.now.sec
20.times do |i|
puts "Hello number #{i + 1}!"
if i == seconds
puts "Oh no, time's up!"
return
end
end
puts "Bye!"
end
greet_20_numbers
The above program doesn't make a lot of sense, but here's how it works:
- It prints 20 times "Hello number ...!" followed by priting "Bye!"
- However, if while printing the above it happens that the number to greet is the number of seconds on the clock, we print "Oh no, time's up!". Note that we don't print "Bye!" in this case because we return from the method.
Let's try to translate the above program to Go:
package main
import "fmt"
import "time"
func times(n int, block func(int)) {
for i := 0; i < n; i++ {
block(i)
}
}
func greet20numbers() {
now := time.Now()
second := now.Second()
times(20, func(i int) {
fmt.Printf("Hello number %v!\n", i)
if i == second {
fmt.Println("Oh, time's up!")
return
}
})
fmt.Printf("Bye!")
}
func main() {
greet20numbers()
}
Running the above program doesn't work like the Ruby program. Can you spot why?
In Go we also use return
to exit the function. But this return
returns from the function we gave to times
, it doesn't return from greet20numbers
! In Ruby, return
returns from the enclosing method, not the enclosing block.
This is the most important difference between Ruby blocks and passing functions around.
For example many languages (C, Java, C#, Go, Rust) have if
, while
or for
. These constructs also have a "block" of sorts where we do things. In the case of if
, whatever we put inside that if
gets executed if the condition holds. For while
it's similar: the "block" gets executed while the condition holds. With for
you can use more complex conditions. And in all of them if you return
from inside these "blocks", you return from the enclosing function. But then you can't define other constructs that behave in the same way: you might be able to pass "blocks" around, but if you use return
then you return from those "blocks", not from the enclosing method.
In Ruby, with blocks, you can create your own constructs that behave like if
, while
or for
would. If you return from inside 3.times do ... end
, you return from the method. If you do:
File.open("some_file.txt") do |file|
file.each_line do |line|
return if some_condition
end
end
then that return
which is nested inside two blocks will return from the enclosing method! Then all of these blocks don't look like anonymous functions anymore, they look like augmented language syntax.
Give me a break
There's one more thing you can do inside blocks that you can't do with regular anonymous functions from other languages: you can break
from them.
For example:
3.times do |i|
puts "Hello number #{i}!"
break if i == 1
end
The above will print "Hello number 0!", then "Hello number 1!" and stop.
Just like you can break
from a while
in many languages, breaking from loops lets you create your own loops that enjoy all the benefits of regular while
loops: you can return from them and you can break from them! This further reinforces the notion that these methods look like new language constructs rather than anonymous functions.
Finally, you can call next
inside a block to go to the next iteration, just like you can call next
or continue
inside a while
in other languages. However, you can do this with anonymous functions: just return
from them.
There's more to Ruby blocks
Ruby blocks can also be captured and passed around. But I won't cover that here because that does look like anonymous functions or passing functions around. That said, you can also execute blocks in the context of another object, which again I won't cover here. But all of these things make Ruby blocks even more versatile, useful and fun to use!
Other languages that have blocks
Of course Crystal has blocks and they can work like Ruby. Here's the sample program in Crystal:
def greet_20_numbers
seconds = Time.local.second
puts seconds
20.times do |i|
puts "Hello number #{i + 1}!"
if i == seconds
puts "Oh, it's time to get back to work!"
return
end
end
puts "Bye!"
end
greet_20_numbers
The only difference is that we use Time.local.second
instead of Time.now.sec
, but otherwise it works exactly the same as Ruby.
Kotlin is another language where you can also augment the syntax with methods that receive functions or "blocks" and where using return
actually returns from the enclosing function. This is great! I think Kotlin is a really good language that, and this is just a guess, took a lot of inspiration from Ruby. That said, it seems you can't break
from these like you can in Ruby but there are small workarounds you can use for that.
Coming up next
I'll be talking about Ruby's secret algorithms.
Top comments (1)
Great series Ary, thanks for taking the time to write! Looking forward to more content from the crystal community.