Blocks in Ruby

Subscribe to receive new articles. No spam. Quality content.

Today we're going to discuss simple but interesting topic. We will learn blocks by examples. We will understand what's block and how we can use it to create flexible apps.

We've been using blocks everywhere. It's a code that lives inside {...} or do .. end. There are couple examples:

(1..10).each do |i|
  puts i
end

(1..10).each { |i| puts i }

By convention of ruby code, if we can write block as one line - it's preferable to use {...}. For multiline blocks it's better to use do..end.

Methods can accept blocks and call it when needed. To call block inside a method we use keyword yield. For example:

def foo
  puts "Some foo code"
  yield
  puts "Still there"
end

foo { puts "Hi, i'm block" }

As a result we will see the following lines:

Some foo code
Hi, i'm block
Still there

Let's ckeck why we got this result. We called method foo and passed block into it. Then, inside method foo we called that block using yield. So having that we can imagine that code foo worked in the following order:

def foo
  puts "Some foo code"
  puts "Hi, i'm block" # => yield runs block that we passed
  puts "Still there"
end

We could even pass param to block if we need so:

def log(str)
  puts "LOGGER: #{yield(str)}"
end

log("some data") do |str| 
  "My custom format of #{str}"
end

As a result:

LOGGER: My custom format of some data

Let's try to understand how it works. Method log accepts a string that we want to add to log. Then it accepts block that allows us to define how we want to decorate that string.

So we call method log with a string which we want to decorate and also we pass block which decorates that string.

Inside log method we print log and call block with a string we want to add: yield(str).

Big plus of this approach that we have really flexible logger. Because user can define how to decorate string.

What if we call method log without block:

log("some data") # => `log': no block given (yield) (LocalJumpError)

As we can see it throws an error. Currently our method has to accept block, because we call yield. In Ruby there is a possibility to check if method was called with block or not. This allows us to make block optional.

In our example with a logger, we can create some default decorator, but leave ability to pass custom ones.

def log(str)
  return "LOGGER: #{yield(str)}" if block_given?

  "BASIC LOGGER: #{str}"
end

puts log("some basic data") # => BASIC LOGGER: some basic data

result = log("some data") do |str| 
  "My custom format of #{str}"
end

puts result # => LOGGER: My custom format of some data

Now we have really flexible logger. It accepts custom decorator as a block, but also has basic formatter which will be called if we don't pass block. We check if method was called with block by method block_given?. If block was passed - we call it by yield. If not - use default formatter.

I want to show couple more examples of blocks that might be helpful in every day work.

This approach allows to set default values for object in very explicit way:

class User
  attr_accessor :first_name, :last_name

  def initialize
    yield(self)
  end
end

user = User.new do |u|
  u.first_name = "Adrian"
  u.last_name = "Lewis"
end

puts "#{user.first_name} #{user.last_name}" # => Adrian Lewis

The most interesting part here is initialize method, which just returns self. Inside block we have an access to object we create. So we can set default values to it.

Similar approach we can use to configure objects. For example we have a logger which has couple default settings. If we want to create ability to change it - we can use the same way:

require 'ostruct'

class Logger
  attr_reader :config

  def initialize
    @config = OpenStruct.new

    config.prefix = "LOGGER"
    config.time_format = "%Y.%m.%d"

    yield(config) if block_given?
  end

  def log(str)
    "#{Time.now.strftime(config.time_format)} #{config.prefix}: #{str}"
  end
end

l = Logger.new
puts l.log("With standard config") # => 2016.05.22 LOGGER: With standard config

l = Logger.new do |config|
  config.time_format = "%Y-%d-%m"
  config.prefix = "Custom"
end
puts l.log("Own logger") # => 2016-22-05 Custom: Own logger

I really like this example. We have configurable logger. It's not just allow us to use default settings, but allows us to configure it at the moment of creation.

We can do that because on initialize we pass a block that accepts config param. One line of code: yield(config) if block_given? made our logger much more flexible!

I know that there are many articles about blocks, but I hope that in this post you've found some fresh ideas.

Happy hacking!

Subscribe to receive new articles. No spam. Quality content.

Comments