NEOCLASSIC can be extended by means of C++ code. Such code is used in test descriptions and computed rules.
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:
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.
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:
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:
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.
Next define the concept SuccessfulParty by the following description:
(and Party (testC sameNumberFillers maleGuests femaleGuests))
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.
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
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).
A computed description function class is created
A computed fillers function class is created
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.