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