DEV Community

Ary Borenszweig
Ary Borenszweig

Posted on • Edited on

Why I love Ruby: blocks

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
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

And blocks can receive arguments, so you can also do this:

3.times do |i|
  puts "Hello number #{i}!"
end
Enter fullscreen mode Exit fullscreen mode

The above outputs this:

Hello number 0!
Hello number 1!
Hello number 2!
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
    })
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
cloutiy profile image
yc

Great series Ary, thanks for taking the time to write! Looking forward to more content from the crystal community.