![]() |
Articles Feed |
Categories
Archives
- July 2010 (5)
- June 2010 (4)
- April 2010 (3)
- March 2010 (2)
- February 2010 (2)
- January 2010 (1)
- December 2009 (1)
- October 2009 (2)
- September 2009 (2)
- August 2009 (1)
- July 2009 (5)
- June 2009 (2)
- May 2009 (2)
- April 2009 (8)
- March 2009 (7)
- January 2009 (2)
- December 2008 (3)
- November 2008 (5)
- October 2008 (4)
- September 2008 (6)
- August 2008 (4)
- July 2008 (5)
- June 2008 (5)
- May 2008 (4)
- April 2008 (2)
- February 2008 (4)
- January 2008 (2)
- December 2007 (2)
- November 2007 (2)
- October 2007 (2)
- September 2007 (1)
- August 2007 (3)
- July 2007 (1)
- June 2007 (4)
- May 2007 (7)
- April 2007 (2)
- February 2007 (3)
- January 2007 (3)
- November 2006 (3)
- October 2006 (3)
- September 2006 (17)
- November 2004 (1)
Ruby DSL Blocks
by: micah | May 20th, 2007 |
There’s a common pattern I’ve seen for developing DSLs (Domain Specific Language) in Ruby. It’s used in RSpec, the Statemachine Gem, and Unclebob’s Clean Code talk at RailsConf 2007. I haven’t seen a name for this pattern so I’ll call it the DSL Block Pattern.
RSpec
- describe "Bowling Game" do
- it "should score 0 on a gutter game" do
- game = Game.new
- 20.times { game.roll(0) }
- game.score.should eql(0)
- end
- end
Statemachine
- sm = Statemachine.build do
- trans :locked, :coin, :unlocked
- trans :locked, :pass, :locked
- trans :unlocked, :pass, :locked
- trans :unlocked, :coin, :unlocked
- end
Parser
- parser = Args.expect do
- boolean "l"
- number "p"
- string "d"
- end
Here’s the problem. You’ve got to write code for specific domain such as writing specifications (RSpec), defining a Statemachine, or defining command line arguments (Unclebob’s Clean Code talk). These domains have a contained and well defined terminology set. Often the cleanest, most elegant way to express this code is to create a DSL.
Before diving into the example, let me say that I like coffee as much as the next guy. But I feel lost when ever I go to a Starbucks. As you know, Starbucks has a it’s own language, DSL if you will, for ordering coffee. What follows is a DSL Block for ordering Starbucks coffee.
The general grammar for ordering coffee is: Size, Adjective (optional), Type of Coffee. This is by no means comprehensive but it’s sufficient for the example. So if you wanted to order a large coffee, for example, you would say, Grande Coffee. A small espresso: Short Americano. An extra large mixture of regular and decaffeinated coffee with some half and half: Venti Breve Half Caff.
Given the task to code these coffee orders, I’d like to be able to code it like this:
- Starbucks.order do
- grande.coffee
- short.americano
- venti.breve.half_caff
- end
Ok that looks good, but as you look closely, you’ll start to wonder about those methods, grande, short, and venti “Do they have to be defined on the Kernel?” you may ask. Defining them on the Kernel is a scary prospect. And that may convince you to clutter the syntax by passing an object into the block like this:
- Starbucks.order do |order|
- order.grande.coffee
- order.short.americano
- order.venti.breve.half_caff
- end
This would allow you to define the grande, short, and venti methods on the object passed into the block. Although you do need an object where grande, short, and venti will be defined, you don’t need to add an argument to the block. You’ll find code out there, such as Migrations, that uses this less optimal route. It’s not necessary. The trick to get rid of the argument is below:
- module Starbucks
- def self.order(&block)
- order = Order.new
- order.instance_eval(&block)
- return order.drinks
- end
- class Order
- attr_reader :drinks
- def initialize
- @drinks = []
- end
- def short
- @size = "small"
- return self
- end
- def grande
- @size = "large"
- return self
- end
- def venti
- @size = "extra large"
- return self
- end
- def coffee
- @drink = "coffee"
- build_drink
- end
- def half_caff
- @drink = "regular and decaffeinated coffee mixed together"
- build_drink
- end
- def americano
- @drink = "espresso"
- build_drink
- end
- def breve
- @adjective = "with half and half"
- return self
- end
- private
- def build_drink
- drink = "#{@size} cup of #{@drink}"
- drink << " #{@adjective}" if @adjective
- @drinks << drink
- @size = @drink = @adjective = nil
- end
- end
- end
You can see that the Order object is doing all the work. It’s got the responsibility of interpreting the DSL, so let’s call it the Interpreter Object. The Module::order method simply creates an instance of Order and calls istance_eval on it. This causes the block to execute using the binding of the Order instance. All of the methods on Order will be accessible to the block.
The Interpreter Object can do any number of things as it interprets the DSL. In this case it simply generates a translation for Starbucks newbies. But, the sky’s the limit really.

October 18th, 2008 at 06:32 PM Thanks for pointing out that pattern. Great examples. Do you know any other Ruby DSL patterns? Any good references online about building Ruby DSLs in general?
October 18th, 2008 at 06:32 PM What a great way to describe a DSL and how to do with it. For now I'm in trouble of interpreting DSLs... I thought simple XMLs are simpler to parse than a Ruby-based DSL. I hope that can change. This is really cool... Very useful stuff, Micah! Great article...
October 18th, 2008 at 06:32 PM It takes real talent to take something relatively complex and explain it in such simple terms. Well done. The coffee example was great (even though I can't stand the stuff).
October 18th, 2008 at 06:32 PM I enjoyed your analogy on the language being foreign like Starbucks haha...have you seen the new Dunkin Donuts commercial for them btw? good stuff.