DEV Community

&y H. Golang (he/him)
&y H. Golang (he/him)

Posted on

#GopherDiggingRuby: Intro to blocks in Ruby

When you're coding in Ruby, one piece of syntax you'll see a lot of is blocks, with either the curly braces or do...end syntax. So in this tutorial, we will take a look at how we can use blocks as convenient feature of a Ruby developer's toolbelt, and how that compares to Go.

🐼 Looping over collections with Lola

In Go, the idiomatic way to loop over a slice is with the range keyword, like this:

var doggoTricks = []string{
    "roll over", "speak", "shake paws", "toodle-oo",
}

for _, trick := range doggoTricks {
    fmt.Printf("Lola! Can you %s?\n", trick)
    lola.DoTrick(trick)
}
fmt.Println("Good pupper! Have a biscuit! 🦴")
Enter fullscreen mode Exit fullscreen mode

In the for loop's top line, we loop over the doggoTricks slice one item at a time, putting the current dog trick in the variable trick. Then, inside the block, we use the trick variable to tell my dog Lola to do that trick, and then she does the trick using the function lola.DoTrick. And because of the curly braces block, the varable trick is scoped to only exist inside the for loop.

Ruby does have loops of its own, but another cool, and in my opinion quite satisfying way of looping over a Ruby array is with the Array#each method. Doing the same loop in Ruby looks like this:

doggo_tricks = ['roll over', 'speak', 'shake paws', 'toodle-oo']

doggo_tricks.each do |trick|
  puts "Lola! Can you #{trick}?"
  lola.do_trick trick
end
puts "Good pupper! Have a biscuit! 🦴"
Enter fullscreen mode Exit fullscreen mode

The part do |trick| ... end is the block, and inside the pair of pipe (these |) characters, you name the variable that's scoped to the block. Then, inside the block, you can use that variable, such as printing it with puts and passing it into lola.do_trick.

In addition to looping over arrays, there's also an each method for hashes that works similarly to looping over maps in Go. And each isn't the only method that takes in a block; you can also use blocks on methods like Array#sort to influence how an array is sorted, Array#map to create a new array by running the code in the block on each item in the current array, and Array#filter to return an array of only the items where the block's code would return true.

As an example, if we had code like

with_paws = doggo_tricks.filter { |trick| uses_paws trick }
Enter fullscreen mode Exit fullscreen mode

then the with_paws array would only have tricks where uses_paws is true, which includes toodle-oo and shake paws.

If you read my last tutorial on building a dev.to fetcher in Ruby, by the way, a couple places where we used blocks in this way were:

  • with Array.map to convert an array of Ruby hashes to an array of Article objects
  • walking a Markdown document's nodes

By the way, toodle-oo is Lola's signature trick, and it's really cute!

Animated gif of adorable black and white Havanese dog who looks like a panda standing on her hind legs and waving her front paws up and down

🌺 Having an object only in the part of the code where you need it, as told by making a smoothie

Another great use of blocks in your code is when you want to declare a variable, use it, and then do any clean-up code afterwards, like if you're opening a file, reading it into some kind of data structure, and then closing it when you no longer need that File variable, for example. When a Ruby API has use cases following that pattern, blocks are a common way to do that pattern.

As a more fun example, when it's a hot day out, nothing cools off a sloth like a refreshing hibiscus and cecropia leaf smoothie, fresh from the blender. Without blocks, the Ruby code would look something like this:

blender = get_blender()
blender.add('ice cubes', 'hibiscus flowers', 'cecropia leaves')

blender.serve_drinks()

# now on to more Ruby code for the rest of a sloth's day! 
Enter fullscreen mode Exit fullscreen mode

The one problem with this code is, the blender variable is still in scope, and if this is in the middle of a long section of Ruby code, it's easy to forget to call clean_up(blender) and put_back(blender).

The Go approach to making sure that code gets called would be to either use defer like this:

blender := getBlender()
defer func() {
    cleanUp(blender)
    putBack(blender)
}

blender.Add("ice cubes", "hibiscus flowers", "cecropia leaves")
blender.ServeDrinks()

// now on to more Ruby code for the rest of a sloth's day!
Enter fullscreen mode Exit fullscreen mode

The Go function in the defer statement runs at the end of the Go code block we're in, so no matter what branches of code we take, cleanUp(blender) and putBack() blender will be called.

For a Ruby approach with blocks, let's say that this get_blender function had clean_up(blender) and put_back(blender) built-in. Then, the Ruby code for serving smoothies for sloths would look like this:

get_blender do |blender|
  blender.add(
    'ice cubes', 'hibiscus flowers', 'cecropia leaves'
  )
  blender.serve_drinks
end
Enter fullscreen mode Exit fullscreen mode

We put the code we want to run into the block, and use the variable blender inside the block to add the ingredients and then serve the smoothies. Then, behind the scenes, get_blender calls the cleanup code. The person using our blender API, and the people reviewing their code, don't need to pore over lines of code to make sure all the cleanup code is called, because that's built right in, saving time and bug fixes for your dev team!

In my last tutorial on building a dev.to image link fetcher in Ruby, by the way, a couple places where we used blocks in this way were:

  • Sending an HTTP request to dev.to, deserializing the response from JSON, and then no longer needing the response body to be in-scope
  • Generating a CSV file object, and then inside the block adding the image links as rows of the CSV

🍹 Making your own functions that use blocks

One final thing to look at for demystifying Ruby blocks, is how to make your own code that works with them. To do that, we use a keyword called yield. This keyword confused me when I was first learning Rails, but essentially you can use yield to do the pattern of:

  1. Run any code that's before the yield keyword
  2. yield 0 or more Ruby objects that can be passed into the block
  3. Then, run any code past the yield keyword

Let's try that out by giving the get_blender function a definition:

def get_blender
  blender = take_blender_out_of_cabinet()
  blender.plug_in()

  yield blender
  clean_up blender
  put_away blender
end
Enter fullscreen mode Exit fullscreen mode

We first run take_blender_out_of_cabinet() and blender.plug_in() to get the blender ready. Then, we run yield blender. At that point, the coder using our blender can run a block that has the blender passed in as a variable in the block's scope. We run that code, and then return to the body of the get_blender function, where we call clean_up blender and put_away blender to clean up after the sloths enjoy a nice tasty smoothie!

You don't need to necessarily just use a block once in a function, by the way. Remember how we used the Array filter and each methods with blocks that run on each item in an array? Let's say we had some prep work to do before putting the ingredients into the blender. Before blending the hibiscus flowers and cecropia leaves, let's wash them, which we can do by using yield inside a loop, like this:

class Blender
  def add(*ingredients)
    # do prep work for the smoothie here!
    for ingredient in ingredients
      yield ingredient
      add_ingredient_to_blender ingredient
    end

    self.now_make_the_smoothie!
  end
end
Enter fullscreen mode Exit fullscreen mode

For each ingredient we pass into add, we yield the ingredient to the block passed in before putting it in the blender. Finally, with all that done, we now_make_the_smoothie!

So now, the code for adding our smoothie ingredients would look like this:

blender.add(
  'ice cubes', 'hibiscus flowers', 'cecropia leaves'
) do |ingredient|
  if need_to_wash? ingredient
    wash ingredient
  end
end
Enter fullscreen mode Exit fullscreen mode

In the block we are using, we pass the ingredient into need_to_wash?, and then wash the ones where need_to_wash? returns true (ice cubes are already made of water to begin with so no need to wash them). We run the block for each ingredient one at a time, so with all our smoothie ingredients prepped and put in the blender, we finally call blender.now_make_the_smoothies! to get refreshing hibiscus-cecropia smoothies to get just the energy you need for a long slothful day, brought to you by blocks!

For more information about how yield and yield_self, I recommend looking at this blog post by RubyGuides.

Discussion (2)

Collapse
nevans profile image
nicholas a. evans

If you're using this pattern with resources which need to be cleaned up, don't forget to ensure! :)

def get_blender
  blender = take_blender_out_of_cabinet()
  blender.plug_in()

  yield blender
ensure
  if blender
    clean_up blender
    put_away blender
  end
end
Enter fullscreen mode Exit fullscreen mode

Without ensure, your cleanup can be bypassed with

  • raise exception anywhere later
  • throw tag anywhere down the stack
  • break exits the block it's in
  • return exits the method it's in

Using ensure get's you back to what you're used to with defer.

Collapse
andyhaskell profile image
&y H. Golang (he/him) Author

Thanks for sharing that Ruby idiom, I had not heard about that keyword before!