Hammurabi

Preview:

DESCRIPTION

A Scala rule engine

Citation preview

HammurabiA 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]];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 mario.fusco@gmail.com twitter: @mariofusco

Questions?

Don’t forget to check out Hammurabi at:

http://hammurabi.googlecode.com

Thank you!