next up previous
Next: About this document ... Up: NeoClassic User's GuideVersion 1.0 Previous: The CLASSIC Grammar

Subsections

Extending NEOCLASSIC

NEOCLASSIC can be extended by means of C++ code. Such code is used in test descriptions and computed rules.

Writing Test Functions

 

A test function is required for classic test descriptions and host test descriptions. A test function is actually a C++ class with several member functions, only one of which need be written by the user.

The simplest kind of test function takes no parameters. For example, a test function that tests integers for primality would be written

HostTestFunction0(PrimeTest,primep);

TestVal 
PrimeTest::run(const HostIndividual& ind) 
                             const {
  int n = (int)ind;
  if (n <= 1) {
    return testFalse;
  }
  int bound = (int) sqrt((double)n);
  for (int i = 2; i <= bound; i++) {
    if (((n / i) * i) == n) {
      return(testFalse);
    }
  }
  return testTrue;
}
The first line of this example is a macro that sets up a C++ class for the test function, along with most of the data members and member functions. It also creates a generator for this test function, named primep and adds this name to the collection of host test function generators. The only part that need be written is the run function, which is a function that takes (in this case) a HOST individual and returns an element of TestVal.

This test function would be used in a host test description as follows:

In general a test function is set up using a macro of the formwhere Kind is either Host or Classic, Paramno is the number of parameters the test function has, Class is the name of the class for the test function, Name is the name of the test function in the character interface, and TypeI is the type of the i'th parameter for the test function. Paramno can range from 0 to 2. It can also be suffixed with an L to indicate that the last parameter is constructed from the trailing parameters used in the test description.

The run function for such a test class is written

TestVal 
Class::run(const HostIndividual& ind) const {
  ...
}
or
TestVal 
Class::run(const ClassicIndividual& ind,
           Set<ClassicIndividual> *pdeps,
           Set<ClassicIndividual> *ndeps) 
      const {
  ...
}
this function can refer to data members arg1 and so on to refer to the actual parameters used in the test description. The run member functions of CLASSIC test functions have two extra arguments, that are used to compute dependencies if necessary.


  
Figure 3: atLeast test function
\begin{figure*}
\begin{verbatim}
ClassicTestFunction2(AtLeastTest,atLeast,unsign...
 ...
 return testFalse;
 } else {
 return testMaybe;
 }
}\end{verbatim}\end{figure*}

For example, the test function in Figure 3 mimics the atLeast description constructor.

The run function, when applied to an individual, must return one of the three possible elements of the TestVal class:

testFalse: the individual is inconsistent with this description;
testMaybe: the individual is currently consistent with this description, but if information is added to the individual, the individual may become either inconsistent with the description or definitely described by the description; or
testTrue: the individual definitely satisfies this description.

To guarantee correct behavior, test functions must follow these requirements, which are not enforced by NEOCLASSIC:

Normally, test descriptions are not used alone, but are used as part of and descriptions (see Sections 2.1 and 2.2). For example, suppose the user defines the concept EvenInteger as a subconcept of Integer (a built-in concept--see Section 3.3), with a test function (evenp) that decides whether or not the integer is even:

NEOCLASSIC will first test to see if the individual is an integer, and then an even integer. Test functions can depend on this and can safely perform operations that are dangerous to objects that do not belong to the parents (i.e., the function evenp might blow up if called on a String, but this will never happen since before running the evenp function on an individual, NEOCLASSIC will first run the integerp function, which the concept EvenInteger inherits by being a subconcept of Integer, and if this test fails, evenp will not be called). Thus, the test descriptions inherited from the parent concepts are always guaranteed to be run before the told test descriptions on a concept, although there is no way of determining the ordering of the test descriptions between the different parents, or the ordering between the different told descriptions for a concept.

Consider defining an Employee as a Person with at least 1 employer, and who has an age between 18 and 65. First use the above mechanisms to write a C++ class whose run function checks to see if an integer is within a range.

HostTestFunction2(RangeCheck,rangeCheck,int,int);

TestVal 
RangeCheck::run(const HostIndividual& ind) 
                        const {
  int i = ind;
  if (ind >= arg1 && ind <= arg2 ) {
    return testTrue;
  } else {
    return testFalse;
  } 
}
Next define the concept Employee by the following concept description:

(and Person
		(atLeast 1 employer)
		(all age (and Integer
				(testH rangeCheck 18 65))))
Note that Integer must be specified as part of the all restriction, because the conversion to int would not be valid if the host individual was not an int. Note: this concept could have been defined using an interval (see Section 2.2), but a test function was used for illustrative purposes.

The following is a more complicated example, requiring a three-valued test function. Consider defining a SuccessfulParty as a Party where the number of male guests is the same as the number of female guests. First create a C++ class that takes two roles. It returns testTrue if both roles provably have exactly the same number of fillers, testFalse if both roles provably have a different number of fillers, and testMaybe otherwise. Note that ``provably'' here involves the roles being closed, or some provable generic relation between the atLeast and atMost descriptions on the roles.


  
Figure 4: The sameNumberFillers function
\begin{figure*}
\begin{verbatim}
ClassicTestFunction2(SameNumberFillers,sameNumb...
 ...
 return testFalse;
 } else {
 return testMaybe;
 }
}\end{verbatim}\end{figure*}

Next define the concept SuccessfulParty by the following description:

If the individual party1 is a Party with exactly 8 maleGuests and exactly 8 femaleGuests (whether the guests are known, or just known to exist), then it will be classified as a SuccessfulParty. If the individual party2 is asserted to be a SuccessfulParty, then as long as the same-number-fillers test does not return testFalse on party2, it is consistent for it to be a SuccessfulParty.

Allowable Operations in User Functions

User functions are not allowed to change the knowledge base in any way whatsoever, nor can they depend on the order in which inferences are performed, except as detailed here.

The simplest thing to do within a user function is to ask questions about the properties of the current individual (see the sameNumberFillers test function above). The user can assume that the individual has been completely normalized (see Section 9), and all local inferences have been done (i.e., those not involving any other individuals, and those that don't depend on the firing of rules).

It is trickier to access the properties of related individuals in the knowledge base, since not all inferences may have been done when the test function is run. In addition, if the properties of related individuals change, then unless a test function returns dependencies (see the discussion later), there is no way for NEOCLASSIC to know that it must reclassify the affected individual (since a test function is like a black box, and NEOCLASSIC cannot automatically calculate dependency relationships based on information in a test function).

For example, consider defining the DoctorChild concept as someone who has at least 1 child who is a doctor. First define a test function, doctorChildFn, which checks to see if any filler of the child role is an instance of the Doctor concept.

ClassicTestFunction0(DoctorChildFn,doctorChildFn)

TestVal 
DoctorChildFn::run(const ClassicIndividual & ind
                   Set<ClassicIndividual> *,
                   Set<ClassicIndividual> *) const {
  Role role = "child";   
  Concept doctor = "Doctor";
  IndividualSet fillers = 
      ind.derivedFillers(role);
  ...
     doctor.subsumes(filler);
  ...
Then define the concept DoctorChild as (testC doctorChildFn). If Mary is created as a Doctor, and John is created as someone whose child is Mary, then John will be correctly classified under DoctorChild. However, if Fred is created as someone whose child is Harry, and it is later asserted that Harry is a Doctor, then NEOCLASSIC will not know to reclassify Fred, and thus will not discover that Fred is now a DoctorChild. At some later point, if information is added to Fred, he will be reclassified under DoctorChild.

It is acceptable to produce side effects within test functions, as long as the side effects are outside of NEOCLASSIC. However, test functions are run whenever NEOCLASSIC determines that they need to be run, and this may depend on current implementation details. Thus, it is not meaningful to increment a counter every time a test function is run. The user must not write test functions which produce side effects within NEOCLASSIC. It is not acceptable for a test function to call any NEOCLASSIC functions that modify the knowledge base.

Dependencies for Test Functions

  *Warning*: For Advanced Users Only

During classification, NEOCLASSIC calculates and keeps track of certain dependency relationships. For example, if all of Mary's children's children are known, and they are all Athletes, then Mary would be classified under the concept GrandparentOfAthletes. However, if one of her grandchildren, say Fred, stops being an Athlete at any point (the parent concept Athlete is removed from him), NEOCLASSIC must know to reclassify Mary so that she is no longer under the concept GrandparentOfAthletes. Thus, when Mary is classified under GrandparentOfAthletes, all her children and grandchildren are stored as negative dependencies of Mary, which means that if any information is removed from them, Mary needs to be reclassified.

In addition, if Mary is not classified under GrandparentOfDoctors, because two of her grandchildren, Lou and Sarah, are not known to be Doctors, then Lou and Sarah are stored as positive dependencies of Mary, which means that if any information is added to them, Mary needs to be reclassified.

Since test descriptions are basically black boxes to NEOCLASSIC, i.e., NEOCLASSIC has no way of knowing what goes on inside them, NEOCLASSIC cannot automatically calculate dependencies when an individual is being tested for subsumption against a concept containing a test description. Thus, if a test description uses information about other individuals when it is run on an individual, then if things change about those other individuals, NEOCLASSIC somehow needs to know to reclassify that individual.

This can be done if a test function provides dependency information. Test functions can modify the positive and negative dependency sets passed in to them. As an example of a test function that returns dependencies, see the function cl-test-all-closed?, in library.lisp, which takes a list of roles and/or role-paths, and returns whether or not all the role-paths are closed on the individual. If all role-paths are closed, then any intermediate individual along any path is on the ndeps list (if information is removed from one of these individuals, specifically, that the role is closed, then this test function should no longer return testTrue). Otherwise, if any role-path is non-closed, then

Writing Functions for Computed Rules

The functions for computed rules are similar to test functions, except that they are always run on CLASSIC individuals and return either a Description (for a computed description rule) or an Individual Set (for a computed fillers rule).

Computed Description Rules

A computed description function class is created

The run function for a computed description function takes as arguments a CLASSIC individual. When the rule is fired on a CLASSIC individual, the run function is called on the individual, and it produces a NEOCLASSIC description, which is then added to the individual. The function must be written so that once it is run on an individual, it will always produce the same result, even if new information is added to the individual.

Computed Fillers Rules

A computed fillers function class is created

The run function for a computed fillers function takes as arguments a CLASSIC individual. When the rule is fired on a CLASSIC individual, the run function is called on the individual, and it produces an IndividualSet, whose elements are then added as fillers of the role given in the rule. This role is an implicit first parameter of the function class, and so it can be obtained as the value of arg1. The function must be written so that once it fires on an individual, it will always produce the same list of individuals, even if new information is added to the individual.

An illegal use of a filler rule would be a rule that fires, and the function generates an empty set as the result, but if more information were added to the individual, the function will generate a non-empty set of individuals.


next up previous
Next: About this document ... Up: NeoClassic User's GuideVersion 1.0 Previous: The CLASSIC Grammar
Peter F. Patel-Schneider
7/15/1998