Sometimes we need to make our code more expressive and tidy but at the same time flexible and easier to use. Learning how to use Ruby blocks appropriately will level you up as a Ruby programmer.
Sometimes we like to “sandwich” boilerplate code for various tasks. Maybe we want to keep track of the value or a variable within a block of code. The simplest example are HTML tags:
puts "<title>Audio Bacon</title>"
puts "<body>Some content about the best audio blog on the planet!</body>"
# Now let's have a method that calls the block with yield.
def tag(t)
print "<#{t}>"
print yield
print "</#{t}>"
end
# With blocks it looks cleaner and is more flexible.
tag(:title) { "Audio Bacon" }
tag(:body) { "Some content about the best audio blog on the planet!"}
# Output:
# <title>Audio Bacon</title>
# <body>Some content about the best audio blog on the planet!</body>
We might also duplicate code with similar if-else statements, especially checking boolean values. With blocks, we could separate the concerns a bit and even add the flexibility of performing additional tasks from our result.
class StockTrade
def volume
# Get the volume of a stock
rand(100..100000)
end
def price
# Get price of a stock
rand(5.00..25.00)
end
def stock_action
result = yield
if result
puts "BUY"
# Store into database, email user, etc.
else
puts "SELL"
# Log to file, etc.
end
end
end
s = StockTrade.new
if s.volume > 250
puts "BUY"
else
puts "SELL"
end
if s.price < 10.00
puts "BUY"
else
puts "SELL"
end
# Once you start duplicating code, you know there's a better way (DRY).
# Here we're basically checking true/false values with the same output.
# With blocks we could separate out the concerns and even add functionality.
s.stock_action { s.volume > 250 }
s.stock_action { s.price < 10.00 }
So yield basically runs the code in the block and returns its value. That’s the basic concept. Now with this we could run code under a specific context.
One example of this are the Rails environments: development, test, production. When we run code in the context of one of these environments, we want it to automatically switch us back to our default environment, whether an exception has occurred or not. Here are a few other examples:
- Silence warnings, logs, etc.
- Changing drivers temporarily
- Changing defaults for testing (wait times, values, etc)
- Changing locale, currency, etc.
- Temporarily funneling results to a file
In my example we’ll use a broker’s trading platform. Most of them allow you to trade virtually or on a live interface:
class TradingPlatform
attr_accessor :environment
def initialize
@environment = :live
end
def buy_stock
puts "Buy stock in environment #{@environment}"
end
def sell_stock
puts "Sell stock in environment #{@environment}"
end
def short_stock
puts "Shorting stock in environment #{@environment}"
end
end
t = TradingPlatform.new
t.environment = :virtual
t.buy_stock
t.sell_stock
t.short_stock
t.environment = :live
t.buy_stock
Once you have to toggle between different contexts, that usually means you could simplify with blocks:
class TradingPlatform
attr_accessor :environment
def initialize
@environment = :live
end
def buy_stock
puts "Buy stock in environment #{@environment}"
end
def sell_stock
puts "Sell stock in environment #{@environment}"
end
def short_stock
puts "Shorting stock in environment #{@environment}"
end
def in_environment(e)
original_env = e
@environment = e
yield
ensure
@environment = original_env
end
end
t = TradingPlatform.new
t.in_environment(:virtual) do
t.buy_stock
t.sell_stock
t.short_stock
end
t.in_environment(:live) do
t.buy_stock
end
# Output:
# Buy stock in environment virtual
# Sell stock in environment virtual
# Shorting stock in environment virtual
# Buy stock in environment live
As you could see, the original method is prone to errors while the block pattern encapsulates the toggling in one place and ensures the environment is switched back to the original, even after an exception has occurred.
You get the sense you could do a bit more with this, like managing authentication, a database connection, opening a FTP connection, a URL, opening files, etc. With blocks you could have the code manage its own life cycle. In Ruby, a lot of these blocks are performed with class methods (as opposed to instance methods in the previous examples).
Imagine we want to call an API to make trades for us. A lot of third-party app developers would want this feature. Obviously I need to be authenticated and a connection has to be made. Then my orders will hopefully execute and that connection will close.
class TradeNow
def authenticate(user)
@user = user
puts "#{@user} has been authenticated"
end
def sell_stock
raise "Not authenticated" unless @user
puts "Sold stock"
end
def buy_stock
raise "Not authenticated" unless @user
puts "Bought stock"
end
def sign_out(user)
puts "User has signed out"
end
def self.stock_action(user)
u = TradeNow.new
u.authenticate(user)
return u unless block_given?
begin
yield u
ensure
u.sign_out(user)
end
end
end
TradeNow.stock_action("Jay") do |x|
x.buy_stock
x.sell_stock
end
Pay special attention to the class method. I no longer have to instantiate an object in order to perform actions on this service. Obviously you would include real authentication rather than a username but this should shed some light on the common Ruby idioms you’ll see.
Finally, you’ve probably noticed that some objects are instantiated with a block instead of passing in variables. We see this in ActiveRecord, Rake tasks, and Gemfiles. If you think about how yield works, you’ll get an idea of how that happens. When calling new, Ruby allocates the memory and calls initialize. In initialize we could actually pass the object to the block as a block parameter and instantiate the instance variables that way:
class MyClass
attr_accessor :name, :role, :favorite_food
def initialize
@name = name
@role = role
@favorite_food = favorite_food
yield self if block_given?
end
end
my_object = MyClass.new do |x|
x.name = "Jay"
x.role = "Coder"
x.favorite_food = "Baby back ribs"
end
my_object2 = MyClass.new
my_object2.name = "Thomas"
my_object2.role = "Doctor"
my_object2.favorite_food = "Ramen"
p my_object
p my_object2
I hope this tutorial has helped demystify Ruby blocks in a way where you could apply this to your own code.
Recent Comments