[Warning: This is highly experimental. Consider "Delete Before Reading" ;-)
]
The fact that Jess templates may relate to each other in the same way
as (Java) classes and sub-classes lets you write rules where you use
the base template name in a LHS pattern. Facts of any sub-template
will match this pattern. This made me wonder whether the concept
of (Java) interfaces could be exploited in a similar way, i.e., to
write rules that would refer to an interface being associated with
a number of templates.
It's obvious that a regular implementation of such an interface feature
would require some additions both to Jess' syntax and to its innards.
But I thought that it just might be possible to rig up some userfunctions
that would let me write Jess code coming close enough to such
an implementation so that I could study the usefulness of
interfaces for writing rules.
The first userfunction associates a template with one or more
"interfaces", which are also defined as templates (although they
won't be used to assert any facts).
; define "interfaces"
(deftemplate Address (slot address))
(deftemplate Email (slot email))
(deftemplate Phone (slot phone))
Actual templates are defined as usual:
; Template
(deftemplate NamedEntity (slot name))
(deftemplate Person extends NamedEntity)
(deftemplate Company extends NamedEntity)
(deftemplate UserGroup extends NamedEntity)
By calling the first of my userfunctions we add "interfaces" to
these templates:
(add-interfaces Person Address Email Phone)
(add-interfaces Company Address Phone)
(add-interfaces UserGroup Email)
We could have achieved the same set of templates by extending
NamedEntity into suitable subclasses, but this would not permit
us to write rules as shown below, to handle queries by "name",
to be stored in
(deftemplate Query extends NamedEntity)
The first rule finds regular snail-mail addresses:
(defrule ByMail
(Query (name ?name))
?ne <- (Address (address ?address))
(test (eq ?name (fact-slot-value ?ne name)))
=>
(printout t "mail to " ?name " at: " ?address crlf)
)
Notice that we use the "interface" Address, expecting this to
match both Person and Company facts. - Similarly, a rule for
finding email addresses uses the Email "interface" and should
match Person and UserGroup facts:
(defrule ByEmail
(Query (name ?name))
?ne <- (Email (email ?email))
(test (eq ?name (fact-slot-value ?ne name)))
=>
(printout t "email " ?name " at: " ?email crlf)
)
To make this work, another userfunction has to be called after
all rules (at least those referring to an interface) have been
defined:
(expand-interfaces)
With suitable facts (the full .clp cod is below) and queries giving
the names of Eve and the Rules company, we get this output:
email Eve Ning at: [email protected]
mail to Eve Ning at: 1003 Slot ave 12
call Eve Ning at: 031415926
mail to Rules'r'Us at: 8642 Fast dr 9
call Rules'r'Us at: 027182818
There are several caveats I know about and, most likely, many more that
I don't.
First of all, expand-interfaces creates additional rules. (Although
Jess does a similar thing for "or" CEs, Jess is capable of covering
up her tracks, but the Jess API won't let me.) You may run (ppdefrule *)
to learn what's going on.
Second, it is tricky to combine the slot of the template proper
with those added via the interface definition. The template Person
has indeed all the slots "name", "address", "email" and "phone",
and you can write Person patterns with all of them. But if you want
to combine "name" and "address" or "name" and "email" you'll
have to use the somewhat clumsy approach shown in the rules.
Third, within the CLP code, you must stick to this order: define templates,
add interfaces, define rules, expand interfaces, and, last, assert facts.
Any other order is bound to fail.
And, obviously, shadow facts are excluded from this experiment.
Below is the full clp file and the Java code. Perhaps you are
interested enough to experiment with this approach. It would be
interesting to hear what people think of this feature.
Enjoy ;-)
Wolfgang
=== iftest.clp ===
(load-function at.laune.jess.ExpandInterfaces)
(load-function at.laune.jess.AddInterfaces)
; Interface Address
(deftemplate Address (slot address))
; Interface Email
(deftemplate Email (slot email))
; Interface Phone
(deftemplate Phone (slot phone))
; Template
(deftemplate NamedEntity (slot name))
; Template Person
(deftemplate Person extends NamedEntity)
(add-interfaces Person Address Email Phone)
; Template Company
(deftemplate Company extends NamedEntity)
(add-interfaces Company Address Phone)
; Template UserGroup
(deftemplate UserGroup extends NamedEntity)
(add-interfaces UserGroup Email)
; Template Query
(deftemplate Query extends NamedEntity)
(defrule ByMail
"obtain a mail address"
(Query (name ?name))
?ne <- (Address (address ?address))
(test (eq ?name (fact-slot-value ?ne name)))
=>
(printout t "mail to " ?name " at: " ?address crlf)
)
(defrule ByPhone
"obtain a phone number"
(Query (name ?name))
?ne <- (Phone (phone ?phone))
(test (eq ?name (fact-slot-value ?ne name)))
=>
(printout t "call " ?name " at: " ?phone crlf)
)
(defrule ByEmail
"obtain an email address"
(Query (name ?name))
?ne <- (Email (email ?email))
(test (eq ?name (fact-slot-value ?ne name)))
=>
(printout t "email " ?name " at: " ?email crlf)
)
(expand-interfaces)
(deffacts allTheFacts
(Person (name "John Doe")(address "4711 Rule rd 102")(phone
"012345678")(email "[email protected]"))
(Person (name "Eve Ning")(address "1003 Slot ave 12")(phone
"031415926")(email "[email protected]"))
(Company (name "Acme Inc.") (address "1020 Java pl 1")(phone
"017320508"))
(Company (name "Rules'r'Us")(address "8642 Fast dr 9")(phone
"027182818"))
(UserGroup (name "Jess Users")(email "[email protected]"))
(UserGroup (name "JAXB Users")(email "[email protected]"))
)
(reset)
(assert (Query (name "Eve Ning")))
(run)
(assert (Query (name "Rules'r'Us")))
(run)
=== AddInterfaces.java ===
package at.laune.jess;
import java.util.ArrayList;
import java.util.List;
import jess.Context;
import jess.Deftemplate;
import jess.Funcall;
import jess.JessException;
import jess.Rete;
import jess.RU;
import jess.Userfunction;
import jess.Value;
import jess.ValueVector;
/**
* Implements the Jess userfunction adding interfaces to a template.
* Call the function after the template and the interfaces (also
* rules) have been defined:
*
* (add-interfaces <symbol> <symbol>+)
*
* The first symbol is the name of a template. Subsequent symbols
* identify the interfaces (actually template definitions) to be
* added to the template.
*
* @author Wolfgang Laun
*/
public class AddInterfaces implements Userfunction {
private static final String NAME = "add-interfaces";
private Deftemplate theTemp;
private List<Deftemplate> ifList;
/** Get the name of the Jess function.
* @see jess.Userfunction#getName()
*/
public String getName(){
return NAME;
}
/** Call the userfunction
* @see jess.Userfunction#call(jess.ValueVector, jess.Context)
*/
public Value call( ValueVector vv, Context context ) throws
JessException {
int narg = vv.size() - 1;
// Check that we have at least two elements.
if( narg < 2 )
throw new JessException( NAME, "missing argument(s).", "" );
Rete engine = context.getEngine();
// Get all the templates.
String ifName = vv.get( 1 ).stringValue( context );
theTemp = engine.findDeftemplate( ifName );
if( theTemp == null )
throw new JessException( NAME, "no such deftemplate:", ifName );
if( theTemp.isOrdered() )
throw new JessException( NAME, ifName, " is ordered" );
ifList = new ArrayList<Deftemplate>( narg - 1 );
for( int iArg = 2; iArg <= narg; iArg++ ){
String tempName = vv.get( iArg ).stringValue( context );
Deftemplate tempTemp = engine.findDeftemplate( tempName );
if( tempTemp.isOrdered() )
throw new JessException( NAME, ifName, " is ordered" );
if( tempTemp == null )
throw new JessException( NAME, "no such deftemplate:",
tempName );
ifList.add( tempTemp );
}
String theName = theTemp.getName();
engine.removeDeftemplate( theName );
Deftemplate theCopy = new Deftemplate( theName,
theTemp.getDocstring(), engine );
theCopy.setSlotSpecific( theTemp.isSlotSpecific() );
int nEx = theTemp.getNSlots();
for( int iSlot = 0; iSlot < nEx; iSlot++ ){
String name = theTemp.getSlotName( iSlot );
Value value = theTemp.getSlotDefault( iSlot );
if( theTemp.getSlotType( name ) == RU.SLOT ){
theCopy.addSlot( name, value, "ANY" );
} else {
theCopy.addMultiSlot( name, value );
}
theCopy.setSlotAllowedValues( name,
theTemp.getSlotAllowedValues( name ) );
}
List<String> ifNames = new ArrayList<String>();
for( Deftemplate ifTemp: ifList ){
ifNames.add( ifTemp.getName() );
int nSlots = ifTemp.getNSlots();
for( int iSlot = 0; iSlot < nSlots; iSlot++ ){
String name = ifTemp.getSlotName( iSlot );
boolean found = false;
try {
theTemp.getSlotType( name );
found = true;
} catch( JessException je ){
// That's OK - we shouldn't have a slot with this name!
}
if( found )
throw new JessException( NAME, "duplicate slot ", name
);
Value value = ifTemp.getSlotDefault( iSlot );
if( ifTemp.getSlotType( iSlot ) == RU.SLOT ){
theCopy.addSlot( name, value, "ANY" );
} else {
theCopy.addMultiSlot( name, value );
}
theCopy.setSlotAllowedValues( name,
ifTemp.getSlotAllowedValues( name ) );
}
}
engine.addDeftemplate( theCopy );
ExpandInterfaces.addTemp2Interfaces( theName, ifNames );
return Funcall.NIL;
}
}
=== ExpandInterfaces.java ===
package at.laune.jess;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
import jess.Accumulate;
import jess.ConditionalElement;
import jess.Context;
import jess.Defrule;
import jess.Deftemplate;
import jess.Funcall;
import jess.Group;
import jess.HasLHS;
import jess.JessException;
import jess.Pattern;
import jess.Rete;
import jess.Test1;
import jess.Userfunction;
import jess.Value;
import jess.ValueVector;
/**
* Implements the Jess userfunction expanding interfaces into
* a set of rules, one for each template implementing the interface.
* Call the function after all rules (at least those using interfaces)
* have been defined:
*
* (expand-interfaces)
*
* @author Wolfgang Laun
*/
public class ExpandInterfaces implements Userfunction {
// Associates an interface with the templates implementing it
private static Map<String,List<String>> if2templist =
new HashMap<String,List<String>>();
/**
* Add a template and the interfaces implemented by it to the hash.
* @param temp the template
* @param iflist the interfaces (actually templates)
*/
public static void addTemp2Interfaces( String temp, List<String> iflist
){
for( String intface: iflist ){
List<String> occList = if2templist.get( intface );
if( occList == null ){
occList = new ArrayList<String>();
if2templist.put( intface, occList );
}
occList.add( temp );
}
}
private static final String NAME = "expand-interfaces";
private Deftemplate ifTemp;
private List<Deftemplate> tempList;
/** Get the name of the Jess function.
* @see jess.Userfunction#getName()
*/
public String getName(){
return NAME;
}
private boolean procNodes( Group g, ConditionalElement ce, Deftemplate
repTemp )
throws JessException {
boolean result = false;
int nCe = ce.getGroupSize();
for( int iCe = 0; iCe < nCe; iCe++ ){
ConditionalElement subCE = ce.getConditionalElement( iCe );
if( subCE instanceof Accumulate ){
g.add( subCE );
continue;
}
if( subCE instanceof Group ){
Group subG = (Group)subCE;
Group copySubG = new Group( subG.getName() );
result |= procNodes( copySubG, subG, repTemp );
g.add( copySubG );
}
if( subCE instanceof Pattern ){
// Analyze! Interface -> orGroup!
Pattern pat = (Pattern)subCE;
Deftemplate patTemp = pat.getDeftemplate();
if( patTemp.equals( ifTemp ) ){
// It's the interface!
result = true;
Group theOr = new Group ( Group.OR );
String bound = pat.getBoundName();
Pattern conPat = new Pattern( repTemp );
Iterator<?> testIt = pat.getTests();
while( testIt.hasNext() ){
Test1 t1 = (Test1)testIt.next();
conPat.addTest( (Test1)t1.clone() );
}
theOr.add( conPat );
theOr.setBoundName( bound );
g.add( theOr );
} else {
// Not our interface template, just pass it on
g.add( subCE );
}
}
}
return result;
}
/** Call the userfunction
* @see jess.Userfunction#call(jess.ValueVector, jess.Context)
*/
public Value call( ValueVector vv, Context context ) throws
JessException {
int narg = vv.size() - 1;
// Check that we have at least one element.
if( narg > 0 )
throw new JessException( NAME, "spurious arguments.", "" );
Rete engine = context.getEngine();
// Get all the templates.
for( Map.Entry<String, List<String>> i2tEntry:
if2templist.entrySet() ){
String ifName = i2tEntry.getKey();
ifTemp = engine.findDeftemplate( ifName );
tempList = new ArrayList<Deftemplate>();
for( String tName: i2tEntry.getValue() ){
tempList.add( engine.findDeftemplate( tName ) );
}
boolean moreToDo = true;
while( moreToDo ){
moreToDo = false;
// Pass through all the rules.
Iterator<?> ruleIter = engine.listDefrules();
List<Defrule> ruleList = new ArrayList<Defrule>();
while( ruleIter.hasNext() ){
Defrule aRule = (Defrule)ruleIter.next();
ruleList.add( aRule );
HasLHS aLHS = aRule.getNext();
while( aLHS != null ){
ruleList.add( (Defrule)aLHS );
}
}
for( Defrule rule: ruleList ){
ConditionalElement ce = rule.getConditionalElements();
String nameAppendix = "";
int ruleCount = 0;
for( Deftemplate repTemp: tempList ){
Defrule copy = new Defrule( rule.getName() +
nameAppendix, rule.getDocstring(), engine );
Group g = new Group( Group.AND );
boolean change = procNodes( g, ce, repTemp );
copy.addCE( g, engine );
ruleCount++;
nameAppendix = "&" + ruleCount;
int nAct = rule.getNActions();
for( int iAct = 0; iAct < nAct; iAct++ ){
copy.addAction( rule.getAction( iAct ) );
}
if( ruleCount == 0 ){
engine.removeDefrule( rule.getName() );
}
engine.addDefrule( copy );
if( ! change ) break;
moreToDo = true;
}
}
}
}
return Funcall.NIL;
}
}
=== EOF ===