Hammurabi is an internal domain-specific language (DSL) for rule-based programming implemented in Scala. It allows domain experts to write rules in plain Scala code without learning a new language. Rules are evaluated by a rule engine that executes matching rules on a working memory of facts. The DSL aims to be readable, flexible and leverage Scala features like autocompletion. It also supports priorities, selecting objects, and exiting or failing evaluation. The architecture uses actors for concurrent rule evaluation. Future work includes improving performance using RETE algorithm and providing alternative selection methods.
1. Hammurabi
A Scala rule engine
by Mario Fusco
mario.fusco@gmail.com
twitter: @mariofusco
2. "Any fool can write code that a
computer can understand.
Good programmers write code that
humans can understand“
Martin Fowler
3. Programming can be fun,
so can cryptography;
however they should not
be combined d=document,l=Math.floor,g=[],r=0,z=50,i=[],v=500,y="option
",$=[[],[200,251,299,300,301],[0,49,50,51,99,101,150]];eva
l('~o(e,f){f.appendChild(k=d.createElement(e));return
k}~m(t,x,y){o(y?y:"button",x);k.appendChild(d.createTextNo
de(t));return
k}onload=~(){b=d.body;b.style.margin=0;x=(c=b.children[0])
.getContext("2d");o("br",b);c.width=c.height=v;c.onclick=~
(e){n=l(e.clientX/10)+l(e.clientY/10)*z;f(g[n],n)};c=o("se
lect",b);m("Empty",c,y);m("Glider",c,y);m("Small
Exploder",c,y);(c.onchange=~(){for(a=0;a<z*z;a++)f(0,a,1),
f(1,a);for(a in
y=$[c.selectedIndex])f(0,y[a]+1075)})();m("Play/Pause",b).
onclick=~(){if(r++)r=0,clearTimeout(t);else
u()};(j=m("Faster",b)).onclick=m("Slower",b).onclick=~(){v
*=this==j?.8:1.25}};~f(b,n,w){s=w?1:2;h=w?8:6;x.fillStyle=
(g[n]=!b)?"#000":"#fff";x.fillRect((n%z)*10+s,l(n/z)*10+s,
h,h)}~u(){i=g.slice();for(a=0;a<z*z;a++){s=0;for(h=-
1;h<2;h++)for(y=-
1;y<2;y++)n=y*z+a,b=(a+h)%z,s+=(h|y)&n<z*z&0<n&b<z+h&b>=h&
i[n+h];f(i[a]?s&6^2:s!=3,a)}t=setTimeout("u()",v)}'.replac
e(/~/g,'function '))
4. What a rule-based program is
• A rule-based program is made up of discrete rules, each of
which applies to some subset of the problem
• It is simpler, because you can concentrate on the rules for one
situation at a time
• It can be more flexible in the face of fragmentary or poorly
conditioned inputs
• Used for problems involving control, diagnosis, prediction,
classification, pattern recognition … in short, all problems
without clear algorithmic solutions
Declarative vs. Imperative
6. The golfers problem
• A foursome of golfers is standing at a tee, in a line from left to
right. Each golfer wears different colored pants; one is
wearing red pants.
• The golfer to Fred’s immediate right is wearing blue pants.
• Joe is second in line.
• Bob is wearing plaid pants.
• Tom isn’t in position one or four, and he isn’t wearing the
hideous orange pants.
• In what order will the four golfers tee off, and what color are
each golfer’s pants?”
7. The Jess Solution (1)
(deftemplate pants-color (slot of) (slot is))
(deftemplate position (slot of) (slot is))
(defrule generate-possibilities =>
(foreach ?name (create$ Fred Joe Bob Tom)
(foreach ?color (create$ red blue plaid orange)
(assert (pants-color (of ?name)(is ?color)))
)
(foreach ?position (create$ 1 2 3 4)
(assert (position (of ?name)(is ?position)))
)
)
)
8. The Jess Solution (2)
(defrule find-solution
;; There is a golfer named Fred, whose position is ?p1
;; and pants color is ?c1
(position (of Fred) (is ?p1)) Shared variables oblige to
(pants-color (of Fred) (is ?c1)) have one single BIG rule
[……] Uniqueness of colors and
positions is spread in all rules
;; Bob is wearing the plaid pants
(position (of Bob)(is ?p3&~?p1&~?p&~?p2))
(pants-color (of Bob&~?n)(is plaid&?c3&~?c1&~?c2))
;; Tom is not in position 1 or 4
;; and is not wearing orange
(position (of Tom&~?n)(is ?p4&~1&~4&~?p1&~?p2&~?p3))
(pants-color (of Tom)(is ?c4&~orange&~blue&~?c1&~?c2&~?c3))
)
9. "Domain users shouldn't be writing
code in our DSL but it must be
designed for them to understand
and validate“
Debasish Ghosh
10. The only purpose of languages,
even programming ones
IS COMMUNICATION
11.
12. The Hammurabi Solution (1)
var allPos = (1 to 4).toSet
var allColors =
class Person(n: String) { Set("blue", "plaid", "red", "orange")
val name = n
var pos: Int = _ val assign = new {
var color: String = _ def position(p: Int) = new {
} def to(person: Person) = {
person.pos = p
allPos = availablePos - p
}
}
def color(c: String) = new {
def to(person: Person) = {
person.color = c
allColors = availableColors - c
}
}
}
13. The Hammurabi Solution (2)
import hammurabi.Rule._
val ruleSet = Set(
rule ("Unique positions") let {
val p = any(kindOf[Person])
when {
(availablePos.size equals 1) and (p.pos equals 0)
} then {
assign position availablePos.head to p
}
},
[……]
rule ("Person to Fred’s immediate right is wearing blue pants") let {
val p1 = any(kindOf[Person])
val p2 = any(kindOf[Person])
when {
(p1.name equals "Fred") and (p2.pos equals p1.pos + 1)
} then {
assign color "blue" to p2
}
}
)
14. The Hammurabi Solution (3)
val tom = new Person("Tom")
val joe = new Person("Joe")
val fred = new Person("Fred")
val bob = new Person("Bob")
val workingMemory = WorkingMemory(tom, joe, fred, bob)
RuleEngine(ruleSet) execOn workingMemory
val allPersons = workingMemory.all(classOf[Person])
val tom = workingMemory.firstHaving[Person](_.name == "Tom").get
15. Why an internal DSL?
I am lazy
o I didn't want to implement a parser
o I wanted the Scala compiler to syntactically validate the rules
I wanted Hammurabi's users to be lazier than me
o No need to learn a new language: it's plain Scala
o Leverage all the goodies of your favorite IDE like:
autocompletion, syntax highligthing, …
Scala allows all of us to stay lazy and have a very
readable and flexible DSL at the same time
16. Working with immutable objects
case class Person(name: String, pos: Int = 0, color: String = null)
val assign = new {
def color(color: String) = new {
def to(person: Person) = {
remove(person)
produce(person.copy(color = color))
availableColors = availableColors - color
}
}
def position(pos: Int) = new {
def to(person: Person) = {
remove(person)
produce(person.copy(pos = pos))
availablePos = availablePos - pos
}
}
}
17. Exiting with a result
rule("Person to Joe’s immediate right is wearing blue pants") let {
val p1 = any(kindOf[Person])
val p2 = any(kindOf[Person])
when {
(p1.name equals "Joe") and (p2.pos equals p1.pos + 1)
} then {
p2.color = "blue“
exitWith(p2)
}
}
val result = RuleEngine(ruleSet).execOn(workingMemory).get
18. Making evaluation fail
rule ("Unique positions") let {
val p = any(kindOf[Person])
when {
(availablePos.size equals 0) and (p.pos equals 0)
} then {
failWith("No more positions available for " + p.name)
}
}
19. Changing rule's priority
Sometimes you may find that a particular rule should be
treated as a special case
A rule that reports a security breach might need to fire immediately …
rule ("Important rule") withSalience 10 let { ... }
… and on the other hand, a rule that cleans up unused facts might
only need to run during the idle time
rule ("Negligible rule") withSalience -5 let { ... }
20. Selecting with Boolean functions
rule ("Person to Fred’s immediate right is wearing blue pants") let {
val p1 = any(kindOf[Person])
kindOf[Person] having (_.name == "Fred")
val p2 = any(kindOf[Person])
when {
(p1.name equals "Fred") 1and (p2.pos equals p1.pos + 1)
p2.pos equals p1.pos +
} then {
assign color "blue" to p2
}
}
21. Hammurabi internals
Evaluate
Rule Engine Rule Evaluator
EvaluationFinished (Actor)
RuleSet Rule1
S Working
a Agenda Memory
l RuleExecutor Rule Evaluator
i Evaluate (Actor)
RuleExecutor
e Rule2
n RuleExecutor EvaluationFinished
c
e RuleExecutor
Evaluate Rule Evaluator
(Actor)
EvaluationFinished Rule3
22. How Hammurabi DSL works (1)
case class Rule(description: String,
bind: () => RuleDefinition[_], salience: Int = 0)
case class RuleDefinition[A](condition: () => Boolean,
execution: () => A)
def rule(description: String) = new {
def let(letClause: => RuleDefinition[_]): Rule =
Rule(description, letClause _)
def withSalience(salience: Int) = new {
def let(letClause: => RuleDefinition[_]): Rule =
Rule(description, letClause _, salience)
}
}
rule ("An extremly useful rule") withSalience 5 let {
...
}
23. How Hammurabi DSL works (2)
def when(condition: => Boolean) = new {
def then[A](execution: => A): RuleDefinition =
RuleDefinition(condition _, execution _)
}
rule("Joe is in position 2") let {
val p = any(kindOf[Person])
when {
p.name equals "Joe"
} then {
assign position 2 to p
}
}
def ruleExecution() = {
val ruleDef = rule.bind()
if (ruleDef.condition()) ruleDef.execution()
}
24. Future enhancements
Evaluate use of Scala 2.9 parallel collections instead of actors
Improve performances by implementing the RETE algorithm
Provide alternative way to select objects from the working
memory. For example:
produce(person) as 'VIP
rule ("only for Very Important Persons") let {
val vip = any('VIP)
...
}
Any feedback or suggestion is welcome!
25. Questions?
Don’t forget to check out Hammurabi at:
http://hammurabi.googlecode.com
Thank you!
Mario Fusco
mario.fusco@gmail.com
twitter: @mariofusco