<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/css" href="/stylesheets/rss.css"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/">
  <channel>
    <title>8th Light Blog: Ruby DSL Blocks</title>
    <link>http://blog.8thlight.com/articles/2007/05/20/ruby-dls-blocks</link>
    <language>en-us</language>
    <ttl>40</ttl>
    <description>In the minds of the craftsmen...</description>
    <item>
      <title>Ruby DSL Blocks</title>
      <description>&lt;p&gt;There&amp;#8217;s a common pattern I&amp;#8217;ve seen for developing DSLs (Domain Specific Language) in Ruby.  It&amp;#8217;s used in RSpec, the Statemachine Gem, and Unclebob&amp;#8217;s  Clean Code talk at RailsConf 2007.  I haven&amp;#8217;t seen a name for this pattern so I&amp;#8217;ll call it the &lt;strong&gt;DSL Block Pattern&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RSpec&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;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
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Statemachine&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;sm = Statemachine.build do
    trans :locked, :coin, :unlocked
    trans :locked, :pass, :locked
    trans :unlocked, :pass, :locked
    trans :unlocked, :coin, :unlocked
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;strong&gt;Parser&lt;/strong&gt;&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;parser = Args.expect do
    boolean "l"
    number "p"
    string "d"
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Here&amp;#8217;s the problem.  You&amp;#8217;ve got to write code for specific domain such as writing specifications (RSpec), defining a Statemachine, or defining command line arguments (Unclebob&amp;#8217;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.&lt;/p&gt;

&lt;p&gt;&lt;img src="/files/starbucks.jpg" style="float: right;" width="200"/&gt;
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&amp;#8217;s own language, DSL if you will, for ordering coffee. What follows is a DSL Block for ordering Starbucks coffee.&lt;/p&gt;

&lt;p&gt;The general grammar for ordering coffee is: &lt;em&gt;Size, Adjective (optional), Type of Coffee&lt;/em&gt;.  This is by no means comprehensive but it&amp;#8217;s sufficient for the example.  So if you wanted to order a large coffee, for example,  you would say, &lt;em&gt;Grande Coffee&lt;/em&gt;.  A small espresso: &lt;em&gt;Short Americano&lt;/em&gt;. An extra large mixture of regular and decaffeinated coffee with some half and half: &lt;em&gt;Venti Breve Half Caff&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Given the task to code these coffee orders, I&amp;#8217;d like to be able to code it like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt; Starbucks.order do
    grande.coffee
    short.americano
    venti.breve.half_caff
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Ok that looks good, but as you look closely, you&amp;#8217;ll start to wonder about those methods, &lt;code&gt;grande, short, and venti&lt;/code&gt;  &amp;#8220;Do they have to be defined on the Kernel?&amp;#8221; 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:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;Starbucks.order do |order|
    order.grande.coffee
    order.short.americano
    order.venti.breve.half_caff
end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;This would allow you to define the &lt;code&gt;grande, short, and venti&lt;/code&gt; methods on the object passed into the block.  Although you do need an object where &lt;code&gt;grande, short, and venti&lt;/code&gt; will be defined, you don&amp;#8217;t need to add an argument to the block.  You&amp;#8217;ll find code out there, such as Migrations, that uses this less optimal route.  It&amp;#8217;s not necessary.  The trick to get rid of the argument is below:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;module Starbucks

  def self.order(&amp;amp;block)
    order = Order.new
    order.instance_eval(&amp;amp;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 &amp;lt;&amp;lt; " #{@adjective}" if @adjective
      @drinks &amp;lt;&amp;lt; drink

      @size = @drink = @adjective = nil
    end
  end

end
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;You can see that the &lt;code&gt;Order&lt;/code&gt; object is doing all the work.  It&amp;#8217;s got the responsibility of interpreting the DSL, so let&amp;#8217;s call it the Interpreter Object.  The &lt;code&gt;Module::order&lt;/code&gt; method simply creates an instance of &lt;code&gt;Order&lt;/code&gt; and calls &lt;code&gt;istance_eval&lt;/code&gt; on it.  This causes the block to execute using the binding of the &lt;code&gt;Order&lt;/code&gt; instance.  All of the methods on &lt;code&gt;Order&lt;/code&gt; will be accessible to the block.&lt;/p&gt;

&lt;p&gt;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&amp;#8217;s the limit really.&lt;/p&gt;

&lt;p&gt;&lt;a name="#all_the_source" href="#all_the_source" onclick="document.getElementById('all_the_source').style.display = 'block'"&gt;Show all the source code.&lt;/a&gt;&lt;/p&gt;

&lt;div id="all_the_source" style="display: none;"&gt;
&lt;pre&gt;&lt;code&gt;
    module Starbucks

      def self.order(&amp;block)
        order = Order.new
        order.instance_eval(&amp;block)
        return order.drinks
      end

      class Order

        attr_reader :drinks

        def initialize
          @drinks = []
        end

        def short
          @size = "small"
          return self
        end

        def tall
          @size = "medium"
          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 decaf
          @drink = "decaffeinated 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 mocha
          @drink = "espresso mixed with chocolate, milk, and topped with whipped cream"
          build_drink
        end

        def cappuchino
          @drink = "espresso mixed with milk and topped with steamed milk"
          build_drink
        end

        def no_whip
          @adjective = "without the whipped cream"
          return self
        end

        def non_fat
          @adjective = "with non-fat milk"
          return self
        end

        def breve
          @adjective = "with half and half"
          return self
        end

        def extra_shot
          @adjective = "with an extra shot of espresso"
          return self
        end

        def soy
          @adjective = "with soy milk"
          return self
        end

        def extra_hot
          @adjective = "with super heated milk"
          return self
        end


        private

        def build_drink
          drink = "#{@size} cup of #{@drink}"
          drink &lt;&lt; " #{@adjective}" if @adjective
          @drinks &lt;&lt; drink

          @size = @drink = @adjective = nil
        end
      end

    end

    require File.expand_path(File.dirname(__FILE__) + "/starbucks")

    #### Specs ####

    describe "Starbucks" do

      it "should handle empty orders" do
        Starbucks.order {}.should eql([])
      end

      it "should handle regular coffee with all the sizes" do
        Starbucks.order do
          short.coffee
          tall.coffee
          grande.coffee
          venti.coffee
        end.should eql(["small cup of coffee",
                        "medium cup of coffee",
                        "large cup of coffee",
                        "extra large cup of coffee"])
      end


      it "should deal with types of drinks" do
        Starbucks.order do
          short.decaf
          short.half_caff
          short.americano
          short.mocha
          short.cappuchino
        end.should eql(["small cup of decaffeinated coffee",
                        "small cup of regular and decaffeinated coffee mixed together",
                        "small cup of espresso",
                        "small cup of espresso mixed with chocolate, milk, and topped with whipped cream",
                        "small cup of espresso mixed with milk and topped with steamed milk"])
      end

      it "should handle the adjectives" do
        Starbucks.order do
          short.no_whip.coffee
          short.non_fat.coffee
          short.breve.coffee
          short.extra_shot.coffee
          short.soy.coffee
          short.extra_hot.coffee
        end.should eql(["small cup of coffee without the whipped cream",
                        "small cup of coffee with non-fat milk",
                        "small cup of coffee with half and half",
                        "small cup of coffee with an extra shot of espresso",
                        "small cup of coffee with soy milk",
                        "small cup of coffee with super heated milk",])
      end

    end 
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;</description>
      <pubDate>Sun, 20 May 2007 08:45:00 +0000</pubDate>
      <guid isPermaLink="false">urn:uuid:2c4a8bba-8e80-4918-a66f-727694ba4522</guid>
      <author>Micah</author>
      <link>http://blog.8thlight.com/articles/2007/05/20/ruby-dls-blocks</link>
      <category>Coding</category>
      <category>Micah</category>
      <enclosure type="image/jpeg" length="12655" url="http://blog.8thlight.com/files/starbucks.jpg"/>
    </item>
    <item>
      <title>"Ruby DSL Blocks" by Clinton Forbes</title>
      <description>&lt;p&gt;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&amp;#8217;t stand the stuff).&lt;/p&gt;</description>
      <pubDate>Fri, 15 Feb 2008 12:38:14 +0000</pubDate>
      <guid isPermaLink="false">urn:uuid:d828008f-41c3-457c-b33a-67540c7fa905</guid>
      <link>http://blog.8thlight.com/articles/2007/05/20/ruby-dls-blocks#comment-729</link>
    </item>
    <item>
      <title>"Ruby DSL Blocks" by Hendy Irawan</title>
      <description>&lt;p&gt;What a great way to describe a DSL and how to do with it.&lt;/p&gt;

&lt;p&gt;For now I&amp;#8217;m in trouble of interpreting DSLs&amp;#8230; I thought simple XMLs are simpler to parse than a Ruby-based DSL. I hope that can change.&lt;/p&gt;

&lt;p&gt;This is really cool&amp;#8230;&lt;/p&gt;

&lt;p&gt;Very useful stuff, Micah!&lt;/p&gt;

&lt;p&gt;Great article&amp;#8230;&lt;/p&gt;</description>
      <pubDate>Mon, 19 Nov 2007 03:51:08 +0000</pubDate>
      <guid isPermaLink="false">urn:uuid:19fe0435-cf98-409c-91a7-e043ca3de13b</guid>
      <link>http://blog.8thlight.com/articles/2007/05/20/ruby-dls-blocks#comment-393</link>
    </item>
    <item>
      <title>"Ruby DSL Blocks" by Andy Maleh</title>
      <description>&lt;p&gt;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?&lt;/p&gt;</description>
      <pubDate>Mon, 29 Oct 2007 06:37:45 +0000</pubDate>
      <guid isPermaLink="false">urn:uuid:d2b207be-6ae2-4b84-886e-112376a3276a</guid>
      <link>http://blog.8thlight.com/articles/2007/05/20/ruby-dls-blocks#comment-350</link>
    </item>
  </channel>
</rss>
