Friday, 5 June 2009

The Horn DSL Redux - Part II - Mixins and Blocks

In my last post, I described how I spiked a brief proof of concept that gave me a degree of confidence that I could rewrite the horn Dsl in ironruby.

At the time of writing, the horn Dsl was quite basic and looked like this:

    1 install horn:
    3        description "A .NET build and dependency manager"
    4        build_with msbuild, buildfile("src/horn.sln"), FrameworkVersion35
    5        shared_library "."
    7 dependencies:
    8         @log4net >> @lib

My mission was simple, take the above prose and translate it line for line into Ruby code.

My first bite of the cherry was to define the install method written in boo below:

    1 install horn:
    2        description "A .NET build and dependency manager"

In the boo version of the Dsl the install method gets added to an anonymouse base class via some quite remarkable compiler trickery that you can read about here. One thing we do not have at our disposable in Ruby or more importantly ironruby is a compiler. I would have to rely on the language constructs of the internal Dsl in order to create a fluent human readable syntax.

One way of parsing the install construct would have been to simply define the install method in a seperate code file. In Ruby any method that you define outside of a class or a module is appended onto the object base class that all ruby objects inherit from. This felt dirty and unsatisfactory. I digged deeper into the semantics of Ruby.

A better way soon appeared. I would define the install method in a Ruby module and append this method onto the object base class using a concept known as mixins.

A mixin is Ruby's way of adding the benefits of multiple-inheritance while sticking to a strict object hierarchy. Mixing in is the practice of grouping a set of methods and variables together into a module and inserting those modules as needed into a class. The class incorporates the module into its own capabilities and remembers that the module has been mixed in. Unlike inheritance, mixing in is always a dynamic process. It is not bound to the class definition itself.

A module in Ruby is a unit of code organization that is similar to a namespace in C#. Modules help you organize your code into a hierarchial namespace so that you can group logical bits of coding together.

I have to admit that I copied this idea from the RSpec source but plagarism is the highest compliment. I created the following module structure that I would use to mix in my install method into the object base class.

    1 module MetaBuilder
    2   module Dsl
    3     module Main
    5       def install(name, &block)       
    6         yield self if block_given?
    7       end
    9     end
   10   end
   11 end
   13 include MetaBuilder::Dsl::Main

I will explain the install method in more detail later. The important things to note are:

  • There is an install method on line 5 that is contained in a nested module structure. This method is now available to mix in to other classes.

  • The Ruby include statement on line 13 pulls all the module's regular methods into the class as instance methods.

With this in place I could start to create my first ironruby build script that would contain build instructions that horn can translate into build instructions for a particular component. Below is the start of an DSL instance script written in Ruby that will tell the horn runtime how to build horn itself.

    1 require 'hornbuild'
    3 install :horn do
    4   description "A .NET build and dependency manager"
    5 end

The require statement on line 1 is Ruby's reusable code mechanism that is used to load another code file and import all the class and method definitions and also more importantly any execution statements like the include statement that mixes in the include method. The hornbuild.rb file contains the module definition and install method outlined above.

Let us examine our meagre DSL thus far:

    1 require 'hornbuild'
    3 install :horn do
    4   description "A .NET build and dependency manager"
    5 end

Here we are calling the install method and passing in as arguments two important Ruby constructs that make Ruby a good candidate for an internal Dsl. The first argument is the :horn part which is what is known as a symbol in Ruby. A symbol is identified by the leading colon. With a symbol we've got a way to create a name that we can use in our Dsl and pass around, and it isn't a String. They are most commonly used in hashes which we will explain in a future post. For now think of it like a string constant that can help with readability in an internal Dsl.

The second argument we pass to the install method is what is known as a block in Ruby. A block in Ruby is a closure. The block that is passed to the install method is everything after the do keyword and everything before the end keyword. It is an anonymous code block that is analgous to anonymous delegates and lamdas in C#.

Now let us revisit the install method that will process the arguments that are passed to it:

    9 def install(name, &block)       
   10     yield self if block_given?
   11 end

The name parameter will accept the symbol and the second parameter will accept the block. Blocks are always passed at the end of a method call and are declared implicitly with an ampesand in front of it.

All that is left to do is explain the yield statement on line 10. The yield statement is analogous to the C# yield statement. Basically the yield statement is provided as a way to ease the creation of iterators. It gives control to a user-specified block from within a method's body. Here we are passing control back to the block which is everything after the do keyword and before the end keyword.

I had the beginnings of my ironruby Dsl now. I could progress with a greater confidence.

If any of this is of interest to you then please join the Horn user group for updates or check out the source here.

No comments:

Post a Comment