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.