Hammurabi
 A Scala rule engine




by Mario Fusco
mario.fusco@gmail.com
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]];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 '))
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))          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))
)
"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).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
                                    }
                                  }
                              }
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 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
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

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 { ... }
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
   }
 }
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
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!
Questions?


 Don’t forget to check out Hammurabi at:
http://hammurabi.googlecode.com

               Thank you!
Mario Fusco
mario.fusco@gmail.com
twitter: @mariofusco

Hammurabi

  • 1.
    Hammurabi A Scalarule engine by Mario Fusco mario.fusco@gmail.com twitter: @mariofusco
  • 2.
    "Any fool canwrite code that a computer can understand. Good programmers write code that humans can understand“ Martin Fowler
  • 3.
    Programming can befun, 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-basedprogram 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
  • 5.
    How a rule-basedsystem works
  • 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'tbe writing code in our DSL but it must be designed for them to understand and validate“ Debasish Ghosh
  • 10.
    The only purposeof languages, even programming ones IS COMMUNICATION
  • 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 internalDSL? 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 immutableobjects 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 aresult 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 Sometimesyou 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 Booleanfunctions 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 DSLworks (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 DSLworks (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 Evaluateuse 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 forgetto check out Hammurabi at: http://hammurabi.googlecode.com Thank you! Mario Fusco mario.fusco@gmail.com twitter: @mariofusco