Blocks in Ruby
- 13 November 2016
- Ruby
- Blocks in Ruby
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!