A DSL can change the way you think

Categories: dsl
Posted by Joe Kutner on Sep 03 2007

A domain specific language (DSL) is often nothing more than a clever API. But in many cases, a DSL redefines how a programmer thinks about a program. That is the goal in the Ruleby project. Ruleby is a rule engine written entirely in Ruby. As such, it supports a DSL that is based on the Ruby language. This DSL gives programmers the ability to think about “what” a program is going to do, rather than “how” to it is going to do it.

Martin Fowler would call this an “internal DSL” because it is written inside an existing host language. This differs from applications like the JBoss-Drools rule engine where the primary language is “external.” An external DSL must be parsed and compiled separately from the primary language of the system.

It is important to note that the Drools project does not consider its language to be a DSL. Instead, it is referred to as a Rule Language or Production Language. This is true for the majority of rule engines. However, the lines between DSLs, general purpose languages (GPLs), and even production languages are fuzzy. The Ruleby project has decided to refer to its interface as a DSL, and does not consider this to be a misnomer. The distinction helps Rubyists who are new to rule engines understand the interface.

In order to be qualified as a DSL, a language must have a domain. Indeed, the domain of the Ruleby DSL is conditional logic. It provides an API that makes coding conditional logic more efficient and easier to maintain. In addition, it makes the resulting program faster and more scalable. This is accomplished by breaking down the conditional logic into ‘rules.’ A rule is a two-part structure similar to an ‘if-then’ statement. Consider the following example of a Ruleby rule:

rule [Message, :m, m.status == :HELLO] do |r,v|
   puts v[:m].message
end

This rule checks for the existence of a Message object with a status property that is equal to :HELLO. The block parameter will be executed for each instance that this pattern matches. A rule like this defines ‘what’ a program does, and leaves ‘how’ it will be done up to the rule engine.

The rule method

The Ruleby DSL is built around the rule method. This method, illustrated above, takes 4 parameters:
  1. An optional Symbol that is a unique identifier for the rule (not used above)
  2. An optional Hash that contains rule attributes (not used above)
  3. A variable number of parameters representing patterns (a single Array above )
  4. A block that will be executed if the rule is satisfied

It is the third parameter that makes the rule method worth discussing. This parameter is dynamic because it accepts a variable number of arguments, which can be of two different types: Arrays or Strings. Each type provides a different advantage and syntax.

Using Array parameters

The example above uses the Array parameter. Its syntax is designed to be efficient for developers. Because the condition is separated into items of an array the programmer is able to use a Ruby editor to enforce some of the syntax . Each Array begins with a class type, and is followed by an optional handle for the object (so that it can be referenced in the block). Next, the Array contains any number of conditions that must be true in order for the pattern to match. The example above contains only one condition:

m.status == :HELLO

This expression does not return true or false. Instead, Ruleby parses the expression and evaluates it at a later time. The method_missing feature in Ruby makes this possible. The class represented by m does not have any methods. So when status is invoked the method_missing implementation receives the call. It returns a custom class that overrides the == method.

Using String parameters

Instead of using an Array, the example above could have passed a String with the same semantics:

rule 'For each Message as :m where #status == :HELLO' do |r,v|
   puts v[:m].message
end

Using the String parameter makes the rule more human readable. This syntax is designed be accessible to non-programmers. In many uses of a rule engine, non-technical experts are in charge of maintaining the business logic that makes up a program.

The technical details of parsing the String parameters are less interesting than the Array syntax. The Ruleby DSL interpreter parses the Strings using regular expressions, and evaluates the conditions at a later time. This job is made significantly easier due to Ruby’s support for regular expression matching.

One disadvantage of using the String parameter is that a Ruby interpreter will not catch most syntax errors.

Advanced rules

The example shown above is rather simple. But rules with more complicated conditions illustrate interesting details. Take for example the following rule from a program that calculates the Fibonacci sequence:

rule :calculate, {:priority => 5},
  'Fibonacci as :f1 where #value!=-1 #&& #sequence as :s1',
  'Fibonacci as :f2 where #value!=-1 #&& #sequence==#:s1+1 as :s2',
  'Fibonacci as :f3 where #value==-1 #&& #sequence==#:s2+1' do |e,vars|
    vars[:f3].value = vars[:f1].value + vars[:f2].value
    e.modify vars[:f3]
    e.retract vars[:f1]
    puts vars[:f3].sequence.to_s + ' == ' + vars[:f3].value.to_s
end

Using a rule engine is probably the worst way to calculate the Fibonacci sequence—even worse than the classic recursion example. But like the recursion example, it is a good way to demonstrate certain aspects of the system. This rule defines a pattern that relies on the existence of three objects. In the first pattern, there is one condition and one binding:

'Fibonacci as :f1 where #value!=-1 #&& #sequence as :s1'

The first condition checks that the value attribute is not equal to -1, which means that it is uninitialized. Following the first condition is the and operator #&&, which represents a logical concatenation of the two conditions. The second statement is called a binding. The condition is not concerned with the value of sequence. Instead, it binds that value to a variable named :s1. This variable is referenced in the second pattern.

#sequence == #:s1+1 as :s2

This String is translated into a block of Ruby code. So anything that is possible with the Ruby language is possible in these conditions.

Custom DSLs

It is also possible to build a custom DSL on top of the Ruleby language. For example, a rule template could be wrapped in a method that simplified it:

def for_name(name, &block)
  rule [Employee, :e, m.name == name], &block
end

This method could then be invoked several times to create several different rules:

for_name 'Joe' do |e,v| give_raise(v[:e]) end
for_name 'Bob' do |e,v| fire(v[:e]) end

Depending on the domain of the problem, this technique could make the language even more readable to non-programmers.

The most recent release of Ruleby (0.3) includes more documentation on how to use the DSL. And the Ruleby source repository contains examples of each syntax.

The ‘what’ not the ‘how’

Ruleby blends well with both the “don’t repeat yourself” and “convention over configuration” principles that have become fundamental in the Ruby community. It allows developers to seamlessly integrate rule sets into their programs instead of using external rule files, or repetitive control statements. In this way, they can use features of Ruby to program conditional logic declaratively.

A programmer begins to think about the “what” and not the “how” when he or she separates conditional logic from data. Ruleby does this by introducing a declarative paradigm to the Ruby language. Hopefully, this will encourage Rubyists to think about their programs differently.