Summary
As Russ Olsen points out in a recent blog post, there are two ways to build a DSL (domain-specific language): writing a parser for the language, or bending an existing language into a DSL. Olsen provides a thorough example of how to do the latter in Ruby.
Advertisement
Domain-specific languages, or DSLs, have recently become popular as a way to improve developer productivity: A well-designed DSL provides just the language-features needed for a domain-specific task, removing the verbosity of a more general programming language.
The relative merits of DSLs have been discussed on these pages before. And Jim Freeze has highlighted the advantages of using Ruby when implementing a DSL in an article in Artima's Ruby Code & Style, Creating DSLs with Ruby.
More recently, Russ Olsen gave an concise example of how to mold a Ruby program into interpreting a DSL. As Olsen points out, there are two general approaches to defining a DSL:
There are at least two ways to skin the DSL cat. First, you can ... create a parser for your language by hand or by using a tool like ANTLR or YACC or RACC or even do what the authors of ant did and and define your language as a flavor of XML....
[The other approach is] one in which you start with some implementation language, perhaps Ruby, and you simply bend that one language into being your domain specific language. If you are using Ruby to implement your internal DSL, then anyone who writes a program in your little language is actually, and perhaps unknowingly, writing a Ruby program.
The latter approach, which is used in Ruby on Rails, for instance, involves the following general steps, according to Olsen:
Start off by defining your data structures... Next, set up some top level methods which will support the actual DSL language... Next suck in the DSL [program] text with a load statement. Typically the effect of pulling in the DSL text is to fill in your data structures... Finally, after the load, you do whatever it is that the user asked you to do. How do you know what to do? Why by looking in those freshly populated data structures.
Olsen walks his readers through a DSL that evaluates the answers users provide to a set of questions:
question 'Who was the first president of the USA?'
wrong 'Fred Flintstone'
wrong 'Martha Washington'
right 'George Washington'
wrong 'George Jetson'
Olsen observes that the above DSL code snippet is actually valid Ruby syntax, where question, right, and wrong can be interpreted as method names, and the question and answer text as method parameters. Thus a trivial Ruby program to interpret the above DSL is as follows, according to Olsen:
def question(text)
puts "Just read a question: #{text}"
end
def right(text)
puts "Just read a correct answer: #{text}"
end
def wrong(text)
puts "Just read an incorrect answer: #{text}"
end
load 'questions.qm'
This program takes advantage of the Ruby load keyword that loads and then immediately executes the content of the specified file, assuming that the file contains valid Ruby code.
In subsequent iterations, Olsen extends this simple program to load data structures with questions and answers as the program executes, and implements fairly simple logic in a Question and Answer pair of classes to count the number of right and wrong answers the user provided:
class Question
def initialize( text )
@text = text
@answers = []
end
def add_answer(answer)
@answers << answer
end
def ask
puts ""
puts "Question: #{@text}"
@answers.size.times do |i|
puts "#{i+1} - #{@answers[i].text}"
end
print "Enter answer: "
answer = gets.to_i - 1
return @answers[answer].correct
end
end
class Answer
attr_reader :text, :correct
def initialize( text, correct )
@text = text
@correct = correct
end
end
What is your preferred way of implementing a DSL: with a custom parser, or as a superset of a programming language, as in Olsen's example?
In Ruby: As Olsen does it. In anything else I've programmed in: Create a parser for it.
This is the "normal case" for each of those, though - I have done it the other way for both, because that specific case was better implemented the other way.
However, the ease of creating nice DSLs is one of Ruby's greatest strengths.