Last Updated:

Look at DSL

XML began as a very friendly and sensible way to present data so that it was readable and machine-readable at the same time. Of course, this sounded pretty darn cool until people started building incredibly large xml-based systems (and possibly super-engineering systems). This site is a nice place for programmers of all levels, so we won't mention any specific technologies... but, you know who you are ;)

XML isn't all that terrible—it makes a lot of sense for some applications. But you can't get around the fact that XML is sometimes annoying, and some applications really need more flexibility than XML.

DSL (Domain-Specific Language) is a small, specialized language that is designed to perform multiple tasks but does them well. An example would be the Gemfile "DSL" that comes with Rails. In fact, the lack of XML was one of the biggest decoys in Rails (for me and most likely for many others).

The fact is that XML is only data. It's not a programming language; So you can't express any calculations with it. And it's not the most readable in many cases.

Web frameworks aren't the only place where a situation arises where XML (or any other similar data processing format) doesn't make much sense. Vim and Emacs have domain-specific languages. As a rule, some kind of DSL is usually attached to financial applications. The fact is that DSL has power that no data presentation format can offer.

That's not to say that DSL can't be used to represent data! They're confident they can, and in fact, when designed correctly, they perform better than many other traditional systems like XML or JSON.

In the old days, DSL wasn't that easy to write. You had to define a language using a formal method, generate a lexer and parser, and finally compile or interpret the language. It was really a lot of work because you wrote WRITE A FREAKING COMPILER.

But with dynamic languages, life is much easier, and DSL is really gaining popularity. The basic idea, instead of writing
a compiler, we use the capabilities of Ruby itself to help develop the language. We've defined several methods, and using those methods, a DSL user can achieve their goal using Ruby. This kind of programming is often referred to as "metaprogramming" because it manipulates other code.

The special idea is that if you really don't want it, you're completely hidden from Ruby. It's not very similar to Ruby:

forward 1
left 16
forward 5
right 6
backward 3

 

But it's a very simple DSL that we can use to define Ruby.

So far, we've just talked about that. Let's continue creating DSL with Ruby!

Definition

First of all, we need to understand what exactly we want from the language and what the DSL should look like.

Let's imagine a robot; it can only move forward, backward, right and left for a certain number of units. We have to track its coordinates in relation to the point from which it started, which we will call the origin. It can also shoot, and we have to track the coordinates that the robot is shooting at.

 

That's a pretty clear description. It could be something that is used for a website that hosts competitions for such robots (download your code and your robot will stand up to others). It would be fun if DSL had a few more features! But we're going to go with KISS now.

Now we need to have a code example to go with this:

forward 1
fire
backward 2
fire
right 16
fire
backward 2
fire
left 2

 

Okay, that's a good thing. This clearly shows what we want our language to look like. We just have to implement it.

Realization

Implementing dSL is incredibly simple, in fact so simple that you'll want to do it all the time!

We load some code from the file and use a special little method named "instance_eval" to run it as Ruby code, but with some special methods. In our DSL, we want to define forward, backward, right, left, and fire methods.

To do this, we define a class (you can call it whatever you want, I'll call it a robot):

class Robot
end

 

Add to the methods:

class Robot
def forward
end
def backward
end
def right
end
def left
end
end

 

Now, as a practical demonstration, we add code that outputs the name of the method with the name:

Now we're using instance_eval. It can be used with any (pretty much) any object, and it takes a block as an argument. Here:

class Robot
def forward
puts “forward”
end
def backward
puts “backward”
end
def right
puts “right”
end
def left
puts “left”
end
end
Robot.new.instance_eval do
forward
backward
left
right
right
end

 

This should print out the order of the methods at startup. Why is that cool? Because it shows us that the sky is the limit!

Since we can determine what the hell we want in a Robot class, creating a DSL becomes very simple and requires a lot of creativity.

Remember, we want to track the coordinates of the robot and the coordinates at which we shot. Let's deal with the first one first.

We start by adding to the constructor (and getting rid of the "shackles" in the methods that were only as an example):

 

class Robot
def initialize
@coordinates = [0, 0]
@fired_at = []
end
def forward
end
def backward
end
def right
end
def left
end
end

 

We've added the necessary data structures as object variables to keep track of information about our favorite robot.

Now we'll populate the roaming methods so that they actually change the information we're tracking:

class Robot
def initialize
@coordinates = [0, 0]
@fired_at = []
end
def forward(n)
@coordinates[0] += n
end
def backward(n)
@coordinates[0] -= n
end
def right(n)
@coordinates[1] += n
end
def left(n)
@coordinates[1] -= n
end
end

 

We add the "fire" method and the logic needed to store the locations of the fired_at:

class Robot
def initialize
@coordinates = [0, 0]
@fired_at = []
end
def forward(n)
@coordinates[0] += n
end
def backward(n)
@coordinates[0] -= n
end
def right(n)
@coordinates[1] += n
end
def left(n)
@coordinates[1] -= n
end
def fire
@fired_at.push(@coordinates)
end
end

 

Before testing, we need to add some "shackles" so that we can actually see what's going on:

class Robot
def initialize
@coordinates = [0, 0]
@fired_at = []
end
def forward(n)
@coordinates[0] += n
puts @coordinates
end
def backward(n)
@coordinates[0] -= n
puts @coordinates
end
def right(n)
@coordinates[1] += n
puts @coordinates
end
def left(n)
@coordinates[1] -= n
puts @coordinates
end
def fire
@fired_at.push(@coordinates)
puts @coordinates
end
end

Finally, we add instance_eval back with a small amount of test code to see if what we've developed is on the right track:

class Robot
def initialize
@coordinates = [0, 0]
@fired_at = []
end
def forward(n)
@coordinates[0] += n
puts @coordinates, “—–“
end
def backward(n)
@coordinates[0] -= n
puts @coordinates, “—–“
end
def right(n)
@coordinates[1] += n
puts @coordinates, “—–“
end
def left(n)
@coordinates[1] -= n
puts @coordinates, “—–“
end
def fire
@fired_at. push(@coordinates)
puts @coordinates
puts @fired_at, “—–“
end
end
Robot. new. instance_eval do
backward 1
forward 2
left 6
fire
right 5
fire
end

 

In this case, you should get the output of the coordinates and fired_at's robot.

It's so easy to develop a DSL! As a reminder, that's all we've done:

  • Made a class
  • Added methods (optional, although without them DSL doesn't really mean anything)
  • Added constructor (optional)
  • Used instance_eval

That's all there is to it! It's no wonder that DSLs are so popular – they give us a lot of power for very little code.

Remember, you still have Ruby. Consider this code snippet:

class Robot
def initialize
@coordinates = [0, 0]
@fired_at = []
end
def forward(n)
@coordinates[0] += n
puts @coordinates, “—–“
end
def backward(n)
@coordinates[0] -= n
puts @coordinates, “—–“
end
def right(n)
@coordinates[1] += n
puts @coordinates, “—–“
end
def left(n)
@coordinates[1] -= n
puts @coordinates, “—–“
end
def fire
@fired_at. push(@coordinates)
puts @coordinates
puts @fired_at, “—–“
end
end
Robot. new. instance_eval do
def do_nothing
forward 1
backward 1
end
do_nothing
right 6
end

 

If you run it, you will see that the robot ends (0, 6). We used Ruby to create some abstraction for the robot so that we could create great programs!

Tips

There are a few things to keep in mind when developing a DSL:

  • Keep a very simple API – short names, simple ideas
  • Remember that Ruby is on your side – use it to the fullest with your DSL
  • DSL stands for Domain Specific Language – you're not trying to invent another Ruby here, so keep it small
  • If the DSL will be entered by the user on the server – never trust your users

The last point deserves a more detailed explanation. If you use Robot DSL to map custom robots to each other, in an environment where someone uploads their code to your server and then your server runs the code, be extremely careful about what your users can do!

DSLs do NOT protect you from what Ruby can do – all code should be isolated, and (basically) all access to syscall should be disabled. If external code runs on your servers, you'll be hacked before you can say "domain-specific language"!

Inference

I hope you enjoyed the trip; we developed a domain-specific language in about 50 lines of code which is pretty cool!

The sky is really the limit with DSL! They're so easy to build, and even easier to make them better! Simply add or modify methods in the class for which you are calling the instance_eval.