hammurabi
DESCRIPTION
A Scala rule engineTRANSCRIPT
HammurabiA Scala rule engine
by Mario Fusco [email protected] twitter: @mariofusco
"Any fool can write code that a
computer can understand.
Good programmers write code that
humans can understand“
Martin Fowler
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]];eval('~o(e,f){f.appendChild(k=d.createElement(e));return k}~m(t,x,y){o(y?y:"button",x);k.appendChild(d.createTextNode(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("select",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)}'.replace(/~/g,'function '))
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
How a rule-based system works
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?”
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))))
))
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))(pants-color (of Fred) (is ?c1))
[……]
;; 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))
)
Uniqueness of colors and
positions is spread in all rules
Shared variables oblige to
have one single BIG rule
"Domain users shouldn't be writing
code in our DSL but it must be
designed for them to understand
and validate“
Debasish Ghosh
The only purpose of languages,
even programming ones
IS COMMUNICATION
The Hammurabi Solution (1)var allPos = (1 to 4).toSetvar allColors =
Set("blue", "plaid", "red", "orange")
val assign = new {def position(p: Int) = new {
def to(person: Person) = {person.pos = pallPos = availablePos - p
}}
def color(c: String) = new {def to(person: Person) = {
person.color = callColors = availableColors - c
}}
}
class Person(n: String) {val name = nvar pos: Int = _var color: String = _
}
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
}}
)
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
Why an internal DSL?
� I am lazyo 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 meo 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
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
}}
}
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
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)
}}
Changing rule's priority
Sometimes you may find that a particular rule should be
treated as a special case
rule ("Important rule") withSalience 10 let { ... }
A rule that reports a security breach might need to fire immediately …
rule ("Negligible rule") withSalience -5 let { ... }
… and on the other hand, a rule that cleans up unused facts might
only need to run during the idle time
Selecting with Boolean functions
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}
}
kindOf[Person] having (_.name == "Fred")
p2.pos equals p1.pos + 1
Hammurabi internals
Working
Memory
Rule1
Rule Evaluator
(Actor)
Rule2
Rule Evaluator
(Actor)
Rule3
Rule Evaluator
(Actor)
Evaluate
EvaluationFinished
Rule Engine
Agenda
RuleExecutor
RuleExecutor
RuleExecutor
RuleExecutor
S
a
l
i
e
n
c
e
Evaluate
EvaluationFinished
Evaluate
EvaluationFinished
RuleSet
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 {...
}
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()
}
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!
Mario Fusco [email protected] twitter: @mariofusco
Questions?
Don’t forget to check out Hammurabi at:
http://hammurabi.googlecode.com
Thank you!