项目作者: vjache

项目描述 :
Kotlin Language Integrated Production System
高级语言: Kotlin
项目地址: git://github.com/vjache/klips.git
创建时间: 2016-10-04T22:21:58Z
项目社区:https://github.com/vjache/klips

开源协议:Apache License 2.0

下载


Kotlin Language Integrated Production System (KLIPS)

Keywords

Rule Engine, Logical Programming, Declarative Programming, Rete, AI Engine, DSL

Right off the bat!

  1. class MyRules : RuleSet() {
  2. init {
  3. rule(group = "GrandFatherRule") {
  4. val p1 = ref<String>() ; val p2 = ref<String>() ; val p3 = ref<String>()
  5. +FatherOf(p1, p2)
  6. +FatherOf(p2, p3)
  7. effect {
  8. +GrandFatherOf(p1, p3)
  9. }
  10. }
  11. rule(group = "GrandFatherRule") {
  12. val p1 = ref<String>() ; val p2 = ref<String>() ; val p3 = ref<String>()
  13. +FatherOf(p1, p2)
  14. +MotherOf(p2, p3)
  15. effect {
  16. +GrandFatherOf(p1, p3)
  17. }
  18. }
  19. rule(group = "SiblingsRule") {
  20. val p1 = ref<String>() ; val p2 = ref<String>() ; val p3 = ref<String>()
  21. +FatherOf(p1, p2)
  22. +FatherOf(p1, p3)
  23. guard(p2 gt p3)
  24. effect {
  25. +SiblingOf(p2, p3)
  26. }
  27. }
  28. rule(group = "SiblingsRule") {
  29. val p1 = ref<String>() ; val p2 = ref<String>() ; val p3 = ref<String>()
  30. +MotherOf(p1, p2)
  31. +MotherOf(p1, p3)
  32. effect {
  33. +SiblingOf(p2, p3)
  34. }
  35. }
  36. rule(group = "SiblingsSymmetryRule") {
  37. val p1 = ref<String>() ; val p2 = ref<String>()
  38. +SiblingOf(p1, p2)
  39. effect {
  40. +SiblingOf(p2, p1)
  41. }
  42. }
  43. }
  44. }
  45. val r = MyRules()
  46. r.input.flush {
  47. +FatherOf("p1", "p2")
  48. +FatherOf("p2", "p3")
  49. +FatherOf("p1", "p22")
  50. }

Still interesting? Well, lets dive!

Motivation

This library is inspired by the CLIPS (C Language Integrated Production
System), hence KLIPS does for Kotlin the same thing as CLIPS for C.
Why Kotlin? Kotlin is from JVM clan and more expressive than Java but
simpler than Scala. Kotlin have a good enough capabilities to design a
DSL allowing more or less smooth mix of rule declarations with ‘ordinary’
Kotlin code. Other reason is that Kotlin become a ‘weapon’ of choice for
more and more programmers for an Android devices. I believe this library
would be a helper for programmers developing AIs for their games for
Android.

Introduction

KLIPS provides DSL to specify a production rules and a rule engine
to trigger them. Rule have its left hand side (LHS) which specifies a
condition under which a rule will be activated, and a right hand side
(RHS) which specifies a what to do if rule decided to apply. Engine
solves a system of a rules against working memory in a reactive way i.e.
when it is changed. Working memory (WM) is a set of facts. Facts are
pieces of a data describing the state of a world of a particular program,
hence they are specific for each program and defined by a programmer.
Probably, it is more convenient for some domains to treat facts as an
events. Facts are pushed into WM as soon as they become known and if
conditions of some rules are satisfied then those rules become activated.
All activated rules form a collection called an agenda and at some point
a conflict resolution policy is applied. The purpose of a conflict
resolution is to decide which activated rule must be applied first. All
these concepts will be explained in more details bellow.

Fact

Fact is a data piece describing some aspect of a state of a world of
your program. Fact is quite similar to the concept of relation from
‘Relational Algebra’. Technically it is a subclass of the class
org.kotlin.dsl.Fact with some additional requirement about fields.
Lets see example:

  1. class Actor(val id:Facet<Int>, val nrgy:Facet<Int>, val type:Facet<ActorType>) : Fact()
  2. class At(val actorId:Facet<Int>, val cellId:Facet<Int>) : Fact()
  3. class Adjacent(val cellId1:Facet<Int>, val cellId2:Facet<Int>) : Fact()
  4. class Cell(val id:Facet<Int>, val resourceAmount:Facet<Double>) : Fact()
  5. class MoveCommand(val actorId:Facet<Int>, val cellId:Facet<Int>) : Fact()

Please note that the primary constructors contain fields of type
org.kotlin.dsl.Facet. Facets are placeholders which may contain a
concrete constant or a reference (a variable if you wish). Facets
introduced due to we need to be able to reuse the same fact classes to
specify concrete data and patterns which contain references, this
became more clear bellow at the ‘Rule’ topic.
So the facts above, describe a simple domain in which may live some
actors, which may be located at some cell (tile), and some cells may be
adjacent and some other not (topology of space).
Now we put to WM some facts to initialize our world:

  1. input.flush {
  2. +Cell(0.facet, 0.5.facet) // Define cell 0 with some amount of resources
  3. +Cell(1.facet, 0.5.facet)
  4. +Cell(2.facet, 0.5.facet)
  5. +Cell(3.facet, 0.facet)
  6. +Cell(4.facet, 0.facet)
  7. +Adjacent(0.facet, 1.facet) // Define cells 0 and 1 as adjacent
  8. +Adjacent(0.facet, 2.facet) // Define cells 0 and 2 as adjacent
  9. +Adjacent(0.facet, 3.facet) // Define cells 0 and 3 as adjacent
  10. +Adjacent(0.facet, 4.facet) // Define cells 0 and 4 as adjacent
  11. +Actor(1.facet, 100.facet, ActorType.Worker.facet) // Define actor of type Worker with energy 100 units
  12. +At(1.facet, 0.facet) // Place actor at cell 0
  13. }

The instruction input.flush is an API call which modifies a WM state,
please do not bother about that for now, but pay some attention to unary
plus before constructor call and <some const>.facet notation. There
are two basic operations against WM: assert fact and retire fact. The
unary plus is a synonym for an assert operation and means add fact to WM.
The unary minus before fact would be a synonym for retire operation i.e.
remove fact from WM. So, when we want to make program become aware about
some piece of data about world we do assert a fact, and if a fact is no
valid any more we do retire a fact. The notation <some const>.facet is
used to make a constant of type T become a FacetConst<T>.

Rule

Now lets add some rules to our world. Suppose we want to have a rule to
move actors through the world space i.e. cells. Such a rule may sound
like this:

  1. If command 'move some actor to some cell' issued,
  2. and if the target cell is adjacent to the current cell of an actor,
  3. and if an actor have an energy level greater than 5 units,
  4. than place actor on target cell and decrease the energy level of that actor by 5 units.

Such a rule may be described using KLIPS DSL as follows:

  1. // Move rule v1
  2. rule {
  3. // LHS: begin
  4. // Create references
  5. val aid = ref<Int>("aid") // Actor ID reference with name "aid". If name is not specified it will be generagted which is not very convenient for debug printing.
  6. val cid = ref<Int>("cid")
  7. val cid0 = ref<Int>("cid0")
  8. val nrge = ref<Int>("nrgy")
  9. val type = ref<ActorType>("type")
  10. // Describe pattern
  11. +MoveCommand(aid, cid)
  12. +Actor(aid, nrgy, type)
  13. +At(aid, cid0)
  14. +Adjacent(cid, cid0)
  15. // Additional constraint -- energy level must be greater than 5 units
  16. guard(nrgy gt 5.facet)
  17. // LHS: end
  18. // RHS enclosed by effect call
  19. effect { sol ->
  20. -At(aid, cid0) // Actor not at cell cid0 any more
  21. +At(aid, cid) // Actor become at cell cid from now
  22. -Actor(aid, nrgy, type) // Actor data become invalid
  23. +Actor(aid, (sol[nrgy] - 5).facet, type) // Actor data updated with lower energy level
  24. -MoveCommand(aid, cid) // Move command dismissed
  25. }
  26. }

As you can see the rule declaration is done by notation like:

  1. rule {
  2. ... // facts which form a pattern (rule precondition)
  3. guard(...) // guard -- additonal boolean constraint against references (FacetRef's)
  4. effect { sol -> // sol is a solution i.e. map FacetRef -> FacetConst
  5. ... // asserted and retired facts
  6. }
  7. }

The pattern of a rule, i.e. LHS, consists of a facts constructors prefixed
with unary plus or unary minus. The unary plus before the fact constructor
means that fact is a part of a pattern. Unary minus before the fact means
that fact constructor is a part of a pattern but the appropriate bound fact
will be automatically retired when rule effect{} part (RHS) will be
applied to WM. So if we have a rule:

  1. rule {
  2. +F1(x)
  3. effect {
  4. -F1(x)
  5. +F1(func(x))
  6. }
  7. }

Then we can rewrite it as:

  1. rule {
  2. -F1(x)
  3. effect {
  4. +F1(func(x))
  5. }
  6. }

Hence the move rule v1 above may be rewritten in a shorter form as:

  1. val aid = ref<Int>("aid") // Actor ID reference with name "aid". If name is not specified it will be generagted which is not very convenient for debug printing.
  2. val cid = ref<Int>("cid")
  3. val cid0 = ref<Int>("cid0")
  4. val nrge = ref<Int>("nrgy")
  5. val type = ref<ActorType>("type")
  6. // Move rule v2
  7. rule {
  8. -MoveCommand(aid, cid)
  9. -Actor(aid, nrgy, type)
  10. -At(aid, cid0)
  11. +Adjacent(cid, cid0)
  12. guard(nrgy gt 5.facet)
  13. effect { sol ->
  14. +At(aid, cid) // Actor become at cell cid from now
  15. +Actor(aid, (sol[nrgy] - 5).facet, type) // Actor data updated with lower energy level
  16. }
  17. }

The primary goal of an engine is to find such a binding of references
which is satisfying a pattern. Such a binding called ‘solution’. Note
that solution satisfies also a guard optionally existing in an LHS.
Remember school? It is very similar like finding a solution for a system
of an equations, where an equations are facts and variables are references.

After solution is found then the references in an effect{...} are
substituted with constants and asserted or retired against WM. But there
are cases when we want to explicitly specify the constant facet for some
fact at effect{...} like from example above:

  1. effect { sol ->
  2. +Actor(aid, (sol[nrgy] - 5).facet, type)
  3. }

the expression (sol[nrgy] - 5).facet does the computation of a new
value of an energy level decreasing the old one it by 5 and converting
an ordinary Kotlin value to FacetConst by calling an extension
property facet. As you can see, to get the Kotlin value
(i.e. not FacetConst but T) bound to particular reference we use
sol[ref] call.

Now we need another one rule:

  1. // Adjacency symmetry rule
  2. rule {
  3. +Adjacent(cid0, cid)
  4. effect {
  5. +Adjacent(cid, cid0)
  6. }
  7. }

The rule above ensures that if cid0 adjacent to cid then cid
adjacent to cid0. This adjacency symmetry rule required for move rule
successful work. When we initialized our world we have asserted a set of
adjacency facts but we did not asserted their reversed counterparts and
without the adjacency symmetry rule it would lead to a problem of not
triggering the move rule which could be solved by asserting also a reversed
adjacency facts, but it is much better to add a rule generating what we need.

More on ‘guard’

Guard is a way to impose an additional constraints on possible values
denoted by references. For example above have we specified that we are
interested only in those solutions where energy (nrgy) value is greater
than 5:

  1. guard(nrgy gt 5.facet)

the function guard takes a boolean expression made by infix operator
gt (greater than). There are other operators:

  • gt - greater than
  • ge - greater or equal
  • lt - less than
  • le - less or equal
  • eq - equal
  • and - conjunction
  • or - disjunction
    so the more complex guard could look like:
    1. guard( (x gt y) and (z le 5.facet) )
    There is also another way to specify guard:
    1. guard { sol -> sol[nrgy] > 0 }
    I.e. pass to guard boolean function (predicate) accepting solution
    sol. This approach give us more for complicated cases like:
    1. guard { sol -> max(0, sol[nrgy] - sol[nrgy1]) > 0 }

More on ‘effect’

The high order function effect (RHS) may contain any code e.g. side
effect which must be executed if condition met. When rule set is created
the rules are ‘compiled’ into special representation called ‘rete’. At
the phase of such a ‘compilation’ the LHS is executed and pattern (facts)
obtained, but RHS (i.e. lambda passed to the effect) is executed at
‘runtime’ when rule is decided to be applied. In example above we have
no any side effects and using effect only to change a WM.

Rule priority

In a real world rule sets, it is very probable a situation when we have
a several rules activated (in agenda) which have their effects non
commutative. That is the order of an application of the effects may be
important. For this case we want to give a simple hint to the engine —
priority:

  1. rule( priority = 10.5 ) {
  2. ...
  3. }

The lower value the higher priority. If priority is not specified then
it is generated in accordance to the appearance in a rule set, i.e. very
first rule in a rule set have highest priority.

Computation semantics

To prepare a set of rules we must create a subclass of
org.klips.dsl.RuleSet like that:

  1. class MyRules : RuleSet() {
  2. init {
  3. rule {
  4. ...
  5. effect {
  6. ...
  7. }
  8. }
  9. rule {
  10. ...
  11. effect {
  12. ...
  13. }
  14. }
  15. ...
  16. }
  17. }

After that we are able to start to use our rule system as follow:

  1. val r = MyRules()
  2. r.input.flush {
  3. +F1(a,b,c)
  4. +F2(a,b)
  5. ...
  6. }

The method flush allow us to apply a set of facts to the rule system WM.
We should have some high level view about how such an application handled:

  1. 1. facts pushed to the queue `qf`
  2. 2. while `qf` is not empty repeat
  3. 2.1. take a fact from `qf` and apply it to the WM (assert or retire)
  4. 2.2. some rules may be
  5. 2.2.a. activated and pushed into `agenda` along with their solution
  6. 2.2.b. deactivated and purged from `agenda` along with their solution
  7. 3. if `agenda` is not empty
  8. 3.1. take a pair (rule, solution) with highest priority from the `agenda`
  9. 3.2. compute an effect of a rule using the solution and obtain a set of asserted and retired facts
  10. 3.3. push them to the `qf`
  11. 3.4. go to 2.
  12. 4. flush is finished.

So after flush is returns a multiple rules may be triggered and have
applied their effects.