CSC/ECE 517 Fall 2013/ch1 1w30 nn
A domain specific language (DSL) is a computer programming language of limited expressiveness focused on a particular domain. A domain-specific language is created specifically to solve problems in a particular domain and is not intended to be able to solve problems outside it (although that may be technically possible).
Why use DSL ?
DSLs are useful tools – they use the grammar and syntax that closely resembles the lexicon used by the target domain so we can easily express logic specific to the particular problem.They have very specific business domain based implementations. It is designed to be very intuitive and fluent for a domain expert to use.A domain-specific language is similar to programming library and is somewhere between a tiny programming language and a scripting language.
DSLs are popular for two major reasons: improving productivity for developers and improving communication with domain experts.A well-built DSL can make it easier for the developers to understand a complicated workflow thereby increasing their productivity. The communication with the domain experts becomes easier as it provides a bridge between the development of an executable software and a description that the domain experts can understand to know how their requirements have been implemented in the system. This helps to address one of the major concerns in software development- the communication between the developers and the customers.
Lets take for example, a mathematician working on a particular algorithms does not think in loops, iterations and variables but instead he thinks in the form of base cases, recursions and base comparisons. Using a programming or a general purpose language with iterators and loops require him to mentally map the corresponding parameters from his domain to the language he wants to write his code in. Using A DSL for algorithm design can help nullify this translation and provide a much more effective and reusable code library.
Types of DSL
There are two types of DSLs namely Internal and External.
Internal DSL
An internal or embedded DSL is designed and implemented using a host language. A script in an internal DSL is valid code in its general-purpose language, but only uses a subset of the language's features in a particular style to handle one small aspect of the overall system.The programmer need not worry about grammar, parsers, and tools to do the heavy lifting. However, an internal DSL is constrained by the host language, and your DSL is influenced by its host's flexibility, limitations, and idiosyncrasies. The challenge with an internal DSL is to tactfully design the language so that the syntax is within the confines of what the host language allows, yet is as expressive, concise, and fluent as you desire.Ruby has also developed a strong DSL culture: Many Ruby libraries come in the style of DSLs. In particular, Ruby's most famous framework, Rails, is often seen as a collection of DSLs.
Advantages and Disadvantages of Internal DSLs
An internal DSL rides on the syntax of a host language, so the programmer need not spend any time or effort worrying about compiling or parsing. Also as the programmer is largely constrained by the host language, a lot of time is saved designing the syntax of the DSL.or an effective implementation of a DSL a flexible language with great metaprogramming capabilities would be useful.It is better to choose a programming languages which has few restrictions and idiosyncrasies.External DSLs give you better control than internal DSLs when validating DSL syntax. Because you take the effort to define the grammar for your external DSL, that effort also serves to validate the syntax. This is harder to do with an internal DSL because the code is often processed dynamically. You will have to do extensive error checking and do the validation yourself.
External DSL
An external free-standing DSL is a language separate from the main language of the application it works with. Usually, an external DSL has a custom syntax and programmer defined rules for grammar-definitions and parsing the syntax.It is upto the DSL programmer to decide on the language and the tools to implement the DSL. However, using another language's syntax like XML is also common. A script in an external DSL will usually be parsed by a code in the host application using text parsing techniques. Examples of external DSLs that you probably have come across include regular expressions, SQL and XML configuration files for systems like Struts and Hibernate.
Advantages and Disadvantages of External DSLs
An external DSL gives the programmer the liberty to design the syntax of his/her language in exactly the way desired. They can select the language's symbols, operators, constructs, and structure as they fit their domain. On the downside, the programmer has to define the grammar for their language and create a compiler to parse and process the syntax and map it to the semantics they expect. An external DSL gives a programmer a lot of flexibility, but you have to take the time to do the hard work of compiling it.
Building a DSL in Ruby
DSls are quite popular in Ruby because of the nature of the Ruby language itself:
- Especially the ability to use minimal syntax in order to make the code concise.
- Ruby itself is a very expressive language and has readable syntax which facilitates writing of DSLs where one has to come up with their own syntax.
- Most ruby programs are already a DSL because of Ruby's syntax.
Below is the actual Ruby code:
it "must not allow a person to login without the proper credentials " do person.logs_in_with("invalid credentials"); person.must_not(be(person.logged_in?())); end
The above code can also be written as a DSL :
it "must not allow a person to login without the proper credentials " do person.logs_in_with :invalid credentials person.should_not_be_logged_in end
From the above example we can clearly see that the DSL code is more readable and clearly explains what the code means. Notice how we used symbols(:invalid credentials) instead of strings to make it more readable.
How to build a DSL in ruby
Examples
Internal DSL in Ruby
Following is an example of an internal DSL in Ruby : Imagine you want to write a quiz program, the kind of thing that kids learning Latin or programmers studying for the Ruby Expert Programmer Certification exam might use to drill themselves on questions and answers. We are going to call our little language Quiz'em. A program written in the Quiz'em language prompts the student with multiple choice questions and keeps track of how many the student gets right. Run a Quiz'em program and it just starts asking questions:
Who was the first president of the USA? 1 - Fred Flintstone 2 - Martha Washington 3 - George Washington 4 - George Jetson Enter your answer: The idea of Quiz'em is that we have a special little language that expresses the questions and their answers and an interpreter program which will read the questions and answers and administer the quiz. Now the exact syntax of the question and answer language is not really important, as long as it is not too burdensome. Something like this might do nicely:
question 'Who was the first president of the USA?' wrong 'Fred Flintstone' wrong 'Martha Washington' right 'George Washington' wrong 'George Jetson'
question 'Who is buried in Grant\'s tomb?' right 'U. S. Grant' wrong 'Cary Grant' wrong 'Hugh Grant' wrong 'W. T. Grant' Let's assume that we have the two questions above in a file called, questions.qm and see how we can write that Quiz'em interpreter.
Now we might pull out our regular expressions or parser generator and write a traditional parser for the questions above -- first read a word which should be 'question', then look for a quote, then... But there is an easier way. Looking over the questions and answers above, we realize that they could almost be Ruby method calls. Wait! They could be Ruby method calls: if question, right and wrong are all the names of a Ruby methods, then what we have above is perfectly valid Ruby program -- a series of calls to question, right and wrong, each with a single string parameter. If you are not that familiar with Ruby, I should probably mention that in Ruby both the semicolons at the end of statements and the parenthesis around the argument lists are optional.
Just to get things started, let's see if we can write a little Ruby program that does nothing other than read in the question file. Here is the start of our domain specific language interpreter, a little program called quizm.rb:
#!/usr/bin/env ruby 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'
It doesn't look like much, but the code above captures a lot of the ideas that you need to implement an internal DSL in Ruby. First we have the three methods question, right and wrong. Each of these methods take a single, presumably string argument. But the key bit of code for our DSL is the last statement:
load 'questions.qm'
This statement says to load and execute the contents of the file questions.qm as Ruby program text. This means that all those questions, rights and wrongs, the things that looked like Ruby method calls, are actually going to get sucked into our program and interpreted as Ruby method calls. It is this idea of sucking in the DSL and interpreting it as Ruby that puts the internal into an internal DSL. With that load statement, the interpreter and the Quiz'em program merge. It is very B-movie science fiction. Running quizm.rb, we get the output of all those method calls:
Just read a question: Who was the first president of the USA? Just read an incorrect answer: Fred Flintstone Just read an incorrect answer: Martha Washington Just read a correct answer: George Washington Just read an incorrect answer: George Jetson Just read a question: What is the capital of the UK? Just read a correct answer: London Just read an incorrect answer: Paris Just read an incorrect answer: Washington DC Just read an incorrect answer: Bedrock Building a Quiz
Now that we have our question writer unknowingly writing Ruby method calls, what should we really do inside of the methods? What should question and right and wrong actually do? The answer is that they should remember that they were called -- in other words, they should just set up some data structures. First we need some data structures to set up, so let's create some classes. We will probably need an object that represents the whole quiz. Let's call it... Quiz?
require 'singleton' class Quiz include Singleton def initialize @questions = [] end def add_question(question) @questions << question end def last_question @questions.last end def run_quiz count=0 @questions.each { |q| count += 1 if q.ask } puts "You got #{count} answers correct out of #{@questions.size}." end end
The Quiz class is just a collection of questions. Since we are only doing one quiz at a time I made the Quiz class a singleton. The only slightly complex thing about Quiz is the run_quiz method, which actually asks all the questions and reports the number that the student got right.
Next we will need a class for the questions:
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
A question is just some text ("What is 2 + 2?") along with a collection of answers. Like Quiz, Question is a very simple class; the mechanics of actually asking the question taking up more than half the class.
Finally, we need some answers (who doesn't?). Answers are just a string and a boolean telling if this is a right answer or one of those ugly, wrong ones:
class Answer attr_reader :text, :correct def initialize( text, correct ) @text = text @correct = correct end end
Pulling our DSL together
Now that we have built all of our supporting code, making the Quiz'm DSL actually work is easy. Let's rewrite our original question, right and wrong methods to use the classes we just wrote:
#!/usr/bin/env ruby # require 'quiz' def question(text) Quiz.instance.add_question Question.new(text) end def right(text) Quiz.instance.last_question.add_answer Answer.new(text,true) end def wrong(text) Quiz.instance.last_question.add_answer Answer.new(text,false) end
load 'questions.qm'
Quiz.instance.run_quiz The question method just gets hold of the Quiz singleton instance and adds a new question object to it. Now at the time we actually do this, we haven't collected any of the answers for the question. But not to worry: that is done in the right and wrong methods. These nearly identical methods both go to the Quiz singleton and ask for the last question added to the quiz: Quiz.instance.last_question. When they get that last question, they simply append a new answer to it: right adds a new correct answer, while wrong adds a new incorrect answer.
Finally we have the last two lines of our Quiz'em interpreter:
load 'questions.qm'
Quiz.instance.run_quiz The load statement we have seen before: it just pulls in our question file as Ruby code. The very last line of the program actually runs the quiz.
If you run quizm.rb, your output will look something like this (assuming you have my encyclopedic knowledge of American history):
Question: Who was the first president of the USA?
1 - Fred Flintstone
2 - Martha Washington
3 - George Washington
4 - George Jetson
Enter answer: 3
Question: Who is buried in Grant's tomb?
1 - Cary Grant
2 - Hugh Grant
3 - US Grant
4 - Alan Grant
Enter answer: 3
You got 2 answers correct out of 2.
The structure of the Quiz'em interpreter is actually pretty typical of this style of internal DSL. Start off by defining your data structures: in our case the Quiz class and its friends. Next, set up some top level methods which will support the actual DSL language -- in Quiz'em this was the question, right and wrong methods. Next suck in the DSL text with a load statement. Typically the effect of pulling in the DSL text is to fill in your data structures -- we ended up with a quiz full of questions. 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.
Some popular Ruby DSLs
Some popular examples of Ruby DSLs are as follows:
- Rake : [ http://rake.rubyforge.org/]
- Sinatra :[ http://www.sinatrarb.com/]
- Twibot : [ https://github.com/cjohansen/twibot/tree/master]
Advantages and Disadvantages
Some of the advantages:
- Domain-specific languages allow solutions to be expressed in the idiom and lexicon of the domain.It is possible for the domain experts to themselves define , modify and develop the domain-specific language code.
- It is possible to do validations in DSL’s at the domain level.
- The code in most cases is self-documented
- Domain-specific languages improve quality, productivity, reliability, maintainability, portability and reusability.
Some of the disadvantages:
- There are a lot of overhead costs involved w.r.t developing DSL’s.
- The programmer needs to learn a new language , design, implement, setting and maintaining a scope and maintain a DSL as well as the tools required to develop it.
- Also limited number of experts in a DSL increases the labor costs.
- DSL’s also cause some level of processor efficiency hit as compared to hand-coded software.
- Difficulty of balancing trade-offs between domain-specificity and general-purpose programming language constructs.
- Integration of the DSL with other components of the IT system may be difficult.
- Proliferation of similar non-standard domain-specific languages.Non-technical domain experts can find it hard to write or modify DSL programs by themselves.