PRD Ruleset Example

From RIF
Jump to: navigation, search

Back to PRD Discussions Page

This page is intended to provide a complete ruleset to illustrate the different levels of functionality defined here.

This can serve as an example for several discussion topics:

  • Differences between the simple object model and the frames
  • What are the basic level rule constructs?
  • What are the advanced level rule constructs?
  • How can we support an OO model?

We will first define a simple object model, then a basic level ruleset, and finally an advanced level (object model and additional rule constructs).

The simple object model

We use a Java-like syntax to describe the fields of this object model. This simple model supports:

  • Classes have fields
  • Fields are typed with known simple types.
  • A reasonable set of simple types is supported, certainly including date.
  • Absence of value is represented as null.
  • Nested objects are allowed. For example a customer has an address.
  • There is no method.
  • Simple inheritance of fields could be permitted.
  • A mechanism to attach a global constant, like the constant Today below.

Customer class

class Customer
{
   String name;
   Address adr;
};

A customer has a name (string), and an address. The address is a pointer to another object.

Gary Hallmark 00:50, 9 January 2009 (UTC)

We could axiomitize the class using rules like the following

if ?c#eg:Customer and ?c[eg:name->?n1 eg:name->?n2] and not(?n1 = ?n2)
then do (?x new(?x)) (?x#rif:ClassException ?x[rif:class->eg:Customer rif:cardinalityViolation->eg:name])

if ?c#eg:Customer and ?c[eg:name->?n] and isNotString(?n)
then do (?x new(?x)) (?x#rif:ClassException ?x[rif:class->eg:Customer rif:TypeViolation->eg:name])

if ?c#eg:Customer and ?c[eg:adr->?a] and not(?a # eg:Address)
then do (?x new(?x)) (?x#rif:ClassException ?x[rif:class->eg:Customer rif:TypeViolation->eg:adr])

I suggest a "standard" set of exceptions be asserted.

AdrianPaschke 9:50, 10 January 2009 (UTC)

The main problem with frames as they are define in BLD is that the interpretations are in the strict model-theoretic definition and that they are defined as first-order structures whose points are “flat”, that is devoid of further composition. Objects as we have them in Object-oriented programming such as Java are typically composite structures involving several different structures whose elements have a prior defintion and certain inner composition. The advantage of composite structures over their flat model theoretic counterparts is their hierarchical composition from other familiar objects and the locality of inner objects preserving object identity.

For instance take the following rule

if ?c[eg:addr->?Address[eg:street->"Koenigin-Luisenstr." eg:number->24 eg:zip->12345 eg:city->"Berlin"]] ... then do ...

This inner composition of objects is not supported in BLD.

A rule like

if ?c[eg:addr->?Address] ... then do ...

although correct BLD semantics is wrong since it would work for all costumer objects independent from their local address objects.

However a rule like

if ?Address[eg:street->"Koenigin-Luisenstr." eg:number->24 eg:zip->12345 eg:city->"Berlin"] ?c[eg:adr->?Address] ... then do ...

is also wrong since the address objects are not global objects they are local objects of the customer objects, with a specific object identity.

For instance, there might be a customer object and another object, e.g. a credit card object, in the fact base (working memory):

eg:Adrian[eg:addr->?Address[eg:street->"Koenigin-Luisenstr." eg:number->24 eg:zip->12345 eg:city->"Berlin"]]
eg:CreditCard[eg:addr->?Address[eg:street->"Koenigin-Luisenstr." eg:number->24 eg:zip->12345 eg:city->"Berlin"]]. 

In the BLD semantics, with flat and global frames, this would mean we have two address objects and accordingly the rule above would fire twice. However, the intended semantics is of course that the customer rule should only fire for the a customer with the local address object of this customer and not for a customer with the local address object of the credit card.

Address class

class Address
{
   String city;
   int zipcode;
   String country;
};

An address has a city (string), a zipcode (an int) and a country (string).

Account class

class Account
{
   int num;
   String customer_name;
   float balance;
   float rate;
}

An account has a number num (int), has the customer's name (string), a balance (float), and an interest rate (float). The account owner is paid of the interests computed using the interest rate.

LowAccount class

class LowAccount
{
   int num;
   Date startDate;
   Date endDate;
}

This class represents an account whose balance becomes low. The account is marked by the presence of such an object and the rules will process it.

It has the account number, a start date (date at which the balance became low), and an end date (date at which the account's balance is no longer low).

Christian de Sainte Marie 11:21, 13 January (UTC)

Nonwithstanding the specific style chosen for the XML schema to represent the object model (here is, as an example, such a PRD_Ruleset_Example_Schema, in "every-class-a-top-level-element-and-all-attributes-as-subelements" style), I assume that instances of the various classes, once serialized, will look as follows (in EBNF-like pseudo-schema style):

Customer.

<Customer>
    <name> xs:string </name>
    <adr> Address </adr>
</Customer>

Address.

<Address>
   <city> xs:string </city>
   <zipcode> xs:integer </zipcode>
   <country> xs:string </country>
</Address>

Account.

<Account>
   <num> xs:integer </num>
   <;customer_name> xs;string </customer_name>
   <balance> xs:decimal <balance> <!-- no float in DTB -->
   <rate> xs:decimal <rate>       <!-- no float in DTB -->
</Account>

Low Account.

<LowAccount>
   <num> xs:integer </num>
   <startDate> xs:date </startDate>
   <endDate> xs:date </endDate>?
</LowAccount>

Adrian Paschke 11:30, 13 January (UTC)

In OO models it is allowed to have collections such as lists as value of attributes. For instance, a OO frame such as o[a->list(b,b)]. In BLD this frame would be o[a->b a->b] which reduces to o[a->b] according to the semantics of BLD.

Level one rules

The rule constructs supported by this level will allow to express simple rules for production engines. Tests and simple benchmarks could use this level, and it is easy to provide autonomous examples using this level.

Functionality includes:

  • Simple conditions: match objects in the WM satisfying some tests.
  • Not conditions: absence of objects in the WM satisfying some tests.
  • Boolean expressions of the form (field operator value)
  • Use of logical AND, OR, NOT to combine the tests.
  • Variable bindings, for conditions and other expressions
  • Fields can be directly referenced
  • Supports also object traversal to read nested object's fields.
  • Actions include assert, modify, retract

The rules will be expressed in ILOG JRules syntax, just for illustration purposes.

Christian de Sainte Marie 11:21, 13 January (UTC)

I will assume that the object model is described in an XML schema that validates instances in the form described in the previous section, and that that XML schema is identified by IRI http://example.org/PRD_example.xsd.

I will assume, further, that the Import directive has been extended to include the prefix for the XML data document location, which default to dummy if not provided (see my strawman proposal on combining RIF and XML data/schemas).

I will, finally, assume that the RIF document containing the rules includes the following import directive:

Import(prefix="edm" schema-location="http://example.org/PRD_example.xsd)

I represented all the rules in PRD style, since all the rules include a feature not supported by Core (Retract, New or Not).

Rule bad account

This rule states that if an account has a balance less than 500 and an interest rate greater than 0%, then that account’s interest rate is lowered to 0%.

rule bad_account {
  when {
    a: Account(balance < 500; rate > 0);
  }
  then {
    modify a { rate = 0; }
  }
}

This rule uses:

  • Simple condition with binding on the object
  • Simple tests of the form (field operator value)
  • A modify action to change an object

Gary Hallmark 01:06, 9 January 2009 (UTC)

if ?a # eg:Account and ?a[eg:balance->?b eg:rate->?r] and ?b < 500 and ?r > 0
then do (retract(?a[eg:rate->?r]) assert(?a[eg:rate->0]))

Adrian Paschke 05:06, 10 January 2009 (UTC) Another problem is that we don't have a real typed (sorted) logic in BLD and PRD. The split into an instance term and a frame is incorrect for object systems with a type system.

For instance, take this rule

if ?r # eg:Rate and not ?a[eg:balance->?b eg:rate->?r] and ?b < 500 and ?r > 0
then do (retract(?a[eg:rate->?r]) assert(?a[eg:rate->0]))

Now assume there exists a rate object, e.g. a globally define rate, but there is no account object in the fact base (working memory). Accordingly, the rule will fire. Now assume we don't have any rate object in the working memory, the rule will not fire, since we can not find an instance object of the class (type) eg:Rate. However, the intended semantics is, that the rule fires if we don't have an account object in the working memory independent from the fact that there might be or might be no rate object in the wokring memory, which is an instance of the class Rate.

Christian de Sainte Marie 11:21, 13 January (UTC)

Forall ?a Where And( ?a # edm:/descendant::Account
                     (Exists ?b ?r And( ?a[child::balance->?b]
                                        ?a[child::rate->?r]
                                        External(pred:numeric-less-than(?b 500))
                                        External(pred:numeric-greater-than(?r 0)) ) ) )
    Do((?r ?a[child::rate->?r]) Retract(?a[child::rate->?r]) Assert(?a[child::rate->0]) )

Comment: if we could use the knowledge (from the data model as represented in the XML schema) that the balance and rate attributes are single valued, we could use anonymous variables instead of ?b and?r (thus making the RIF representation of the rule closer to the original), and we could use an assertion with a value replacement semantics, instead of having to retract then assert (where the intermediate state between the retract and the assert is undefined).

Rule raise rate

This rule states that if an account has an interest rate greater than 1% but less than 2%, then that account’s interest rate is raised to 2%.

rule raise_rate {
  when {
    a: Account(r: rate; r > 1; r < 2);
  }
  then {
    modify a { rate = 2; }
  }
}

This rule introduces these new constructs:

  • Binding of a field value ("r"). The value is then used twice.

Christian de Sainte Marie 11:21, 13 January (UTC)

Forall ?a Where And( ?a # edm:/descendant::Account
                     (Exists ?r And( ?a[child::rate->?r]
                                     External(pred:numeric-less-than(?r 2))
                                     External(pred:numeric-greater-than(?r 1)) ) ) )
    Do((?r ?a[child::rate->?r]) Retract(?a[child::rate->?r]) Assert(?a[child::rate->2]) )

Rule start bad

States that if an account has a balance less than 500 and is not yet recorded as a low account (with a null end date), then create the low account, with the current date as start date, and a null end date.

rule start_bad {
  when {
    a: Account(n: num; balance < 500);
    not LowAccount(num == n; end == null);
  }
  then {
    assert LowAccount() { num = n; start = Today; end = null; }
  }
}

This rule introduces these new constructs:

  • A not condition.
  • Usage of null to signify that a field has no value.
  • Usage of the Today constant.

Note: Today is a constant. It represents the date of today. The supposes that there is a mechanism to attach some constants for the rule engine runtime.

Gary Hallmark 01:16, 9 January 2009 (UTC)

Issues:

  • do we need a builtin for Today()? very common in real systems (c.f. sysdate(), getCurrentDateTime()).

Note that such a builtin has a fixed interpretation for a single history, but of course the interpretation is usually different for different histories.

  • I use xsi:nil for null
if ?a # eg:Account and ?a[num->?n balance->?b] and ?b < 500 and
  not(exists ?l (?l # eg:LowAccount and ?l[num->?n end->xsi:nil]))
then do (?l new(?l)) (?l # eg:LowAccount ?l[num->?n start->rif:Today() end->xsi:nil])

Christian de Sainte Marie 11:21, 13 January (UTC)

Prefix(Utils <namespace where the external function today is defined, e.g. the appropriate Java package>)
Forall ?a Where And( ?a # edm:/descendant::Account
                     (Exists ?b And( ?a[child::balance->?b]
                                     External(pred:numeric-less-than(?b 500)) ) ) )
       ?n Where ?a[num->?n]
   If Not(Exists ?l And( ?l # edm:/descendant:LowAccount
                         ?l[child::num->?n] ))
   Then Do((?l New(?l)) Assert( ?l # edm:/descendant:LowAccount )
                        Assert( ?l[child::num->?n] )
                        Assert( ?l[child::startDate->External(Utils:today())] ) )

Comment. I do not know how to do a constant for "Today", so I used the dynamic version from the next rule instead. I do not think that we need a null value, with frames used to represent objects; at leat not for that example: checking the non existence of an endDate/value pair for the LowAccount instance has the same effect.

Rule end bad

This rule states that if an account with a null end date in the LowAccount relation has a balance of at least 500 in the Account relation, then its end date is set to the current date.

rule end_bad {
  when {
     low: LowAccount(end == null);
     Account(num == low.num; balance >= 500);
  }
  then {
     modify low { end = Today; }
  }
}

Christian de Sainte Marie 11:21, 13 January (UTC)

Prefix(Utils <namespace where the external function today is defined>)
Forall ?l Where And( ?l # edm:/descendant::LowAccount
                     Not(Exists ?e ?l[child::endDate->?e] ) )
       ?a Where And( ?a # edm:/descendant::Account
                     (Exists ?n ?b And( ?l[child::num->?n]
                                        ?a[child::num->?n]
                                        ?a[child::balance->?b]
                                        External(pred:numeric-greater-than-or-equal(?b 500)) ) ) )
   Assert( ?l[child::endDate->External(Utils:today())] )

Rule forget bad one

This rule states that a bad period with an end date that is more than 350 days ago should be removed from the LowAccount relation. Note that this rule is not in [2], but it makes sense and provides an example with a row deletion.

rule forget_bad {
  when {
     low: LowAccount(end != null);
     a: Account(num == low.num; balance >= 2000);
     Customer(name equals a.name; adr.city equals “New York”);
  }
  then {
     retract low;
  }
}

Christian de Sainte Marie 11:21, 13 January (UTC)

Prefix(Utils <namespace where the external function today is defined>)
Forall ?l Where And( ?l # edm:/descendant::LowAccount
                     ( Exists ?e And( ?l[child::endDate->?e]
                                      External(pred:numeric-greater-than(External(func:substract-dates(External(Utils:today()) ?e)) ))
                                                                         350)) ) )
   Retract(?l)

Comment. Here, i translated the rule as it is described in the text, rather than as it is described in the pseudo code, since they differ.

Level two rules

The object model adds support to:

  • Methods
    • The method Utils.today() returns the date of today. This is computed dynamically and is much better than the previous constant.
    • A method to compute the number of days between 2 dates.

The rule constructs can:

  • Invoke methods and use the return values (if any)
  • Nested object fields traversal
  • An example of aggregate using collect.
  • Supports "in" to specify a source of objects (and "from" for a single object).

Christian de Sainte Marie 11:21, 13 January (UTC)

  • Rules start bad, end bad, forget bad, level 2: I did not see the difference with the level 1 rules, so I did not retranslate them;
  • Rules New York bonus and decrease bad: PRD does not have aggregates, collect or else, in the current draft. These rules can, therefore, not be represented in RIF at this stage

Changhai Ke January 28, 2009

  • They use methods, while level 1 do not use methods.

Rule start bad

This rule states that if an account has a balance less than 500 and is not yet recorded with a null end date in the LowAccount relation, then the account is inserted into a LowAccount relation with the current date as start date, and a null end date.

rule start_bad {
  when {
     a: Account(n: num; balance < 500);
     not LowAccount(num == n; end == null);
  }
  then {
     assert LowAccount()
     {
       num == n;
       start = Utils.today();
       end = null;
     }
  }
}

Rule end bad

This rule states that if an account with a null end date in the LowAccount relation has a balance of at least 500 in the Account relation, then its end date is set to the current date.

rule end_bad {
  when {
     low: LowAccount(end == null);
     Account(num == low.num; balance >= 500);
  }
  then {
     modify low {
       end = Utils.today();
     }
  }
}

Rule forget bad

This rule states that a bad period with an end date that is more than 350 days ago should be removed from the LowAccount relation. Note that this rule is not in [2], but it makes sense and provides an example with a row deletion.

rule forget_bad {
  when {
     low: LowAccount(end != null; Utils.dateDiff(today(), end) > 350);
  }
  then {
     retract low;
  }
}

Rule New York bonus

This rule states that when the number of customers living in New York exceeds 1000, then the interest rate of all New York customers’ accounts with a balance greater than 5000 and an interest rate less than 3% is increased by 1%.

rule NY_bonus {
  when {
     sf: collect Customer(adr.city equals "New York")
        where (size() > 1000);
     c: Customer() in sf.elements();
     a: Account(name equals c.name; balance > 5000; rate < 3);
  }
  then {
     modify a { rate += 1; }
  }
}

Notice that:

  • "size()" returns the length of the collection, which must be greater than 1000.
  • "in" allows to seek for objects in a collection.

Gary Hallmark 19:12, 13 January 2009 (UTC) This calls for a proposal for lists or collections. Might be a Core issue (I think our Charter requests list support). I would start with java.util.Collection, allow only isEmpty, contains, and containsAll as predicates, and expose some additional methods (add, remove, retainAll, etc.) as actions.

Rule decrease bad

This rule states that if the total number of low balance days for an account (as recorded in the LowAccount relation) is greater than 50 and its current balance is between 500 and 1000, then its interest rate is set to 1% in the Account.

rule decrease_bad {
  when {
     a: Account(balance > 500 & < 1000);
     collect (new LowDaysCollector()) LowAccount(num == a.num)
        where (total() > 50);
  }
  then {
     modify a { rate = 1; }
  }
}

Notice that:

  • This rule uses a special collector (LowDaysCollector) to collect the LowAccount objects. This is just an illustration, we need to discuss how we want to PRD to support the aggregation.
  • "total()" is a method of the collector.

Other constructs in level two

  • XPath or SPARQL result expressions can be used in the rules.