Using Class Rules in an Ascribe Rule Set

Class rules allow you to share code across rules, and even across Rule Sets. Using Class rules can help you organize your code and make it easier to maintain and reuse.

Introduction to Classes

As I wrote in Authoring Ascribe Rule Sets, you write rules using JScript, a superset of ECMA 3 JavaScript. An important feature of JScript is the introduction of classes. Classes are the foundation of traditional object-oriented programming.

Classes are defined with the keyword class. They can contain properties (technically fields) and methods. Here is an example of a class definition:

class Animal {

  // A property:
  var age: int = 2;

  // A method:
  function GetName() {
    return "Giraffe";
  }
}

Once you have defined a class you can use it in another rule:

var animal = new Animal(); // create an instance of Animal
var name = animal.GetName(); // get its name
var age = animal.age; // get its age

f.println("name:", name); // name: Giraffe
f.println("age:", age); // age: 2

I won’t try to cover the topic of object-oriented programming and the details of the use of classes. You can read up on the subject in the JScript documentation: Creating Your Own Classes. I will cover two of the important use cases for classes: code reuse and performance.

Code Reuse

In my post Using Taxonomies in Ascribe Rule Sets I described how to use taxonomies in a Rule Set. Suppose that we want to use a taxonomy in more than one rule. One way to do this would be to copy the taxonomy code from one rule and paste it into another. This has obvious problems. If we want to make a correction to the taxonomy, we would have to make the same correction in all rules that use the taxonomy. It also makes our rules longer and harder to read.

Let’s suppose that we are interested in knowing whether the topic or expression of a finding contains a sensitive word. We might use this code:

var taxonomy = new Taxonomy();
// Add a list of sensitive words
// We don’t care about the group text, so we use an empty string

taxonomy.AddGroupAndSynonyms("", "<sue>", "<lawyer>", "<suspend>", "<cancel>");

if (taxonomy.IsMatch(f.t)|| taxonomy.IsMatch(f.e)) {
  // We found a sensitive word, take some action here…
}

We might find this logic useful in several rules in our Rule Set. That makes it a candidate for a Class rule. We can make a Class rule:

class Sensitive {
  var taxonomy = new Taxonomy();

  // class constructor
  function Sensitive() {
    taxonomy.AddGroupAndSynonyms("", "<sue>", "<lawyer>", "<suspend>", "<cancel>");
  }

  // Return true if text contains a sensitive word
  function Is(text: String) {
    return taxonomy.IsMatch(text);
  }
}

This class has a constructor that sets up our taxonomy, and a method Is that returns true if the text passed to it contains a sensitive word.

We can now use this class in any rule in our Rule Set. Let’s use it to mark the topic if the expression contains a sensitive word. We can use this Modify Finding rule:

// Mark topics where expression contains sensitive word
var sensitive = new Sensitive();
if (sensitive.Is(f.e)) {
  f.t = "Sensitive: " + f.t;
}

This rule will prepend “Sensitive: ” to the topic of any finding where the expression contains one of our sensitive words.

Performance

Looking at the last example above, you can see that we new up an instance of the Sensitive class every time the rule is invoked. This is OK for lightweight objects like the Sensitive class, but can lead to performance problems if the class contains more initialization code. We can improve our class by using the static keyword to create variables that are members of the class itself, rather than of the instance of the class. Here is a new implementation of the Sensitive class that uses this approach:

class Sensitive2 {
  static var taxonomy = new Taxonomy();

  // class constructor
  static Sensitive2 {
    taxonomy.AddGroupAndSynonyms("", "<sue>", "<lawyer>", "<suspend>",  "<cancel>");
  }

  // Return true if text contains a sensitive word
  static function Is(text: String) {
    return taxonomy.IsMatch(text);
  }
}

We have simply added the static keyword to the property and method of the class, and modified the constructor by adding the static keyword and removing the parentheses for the empty argument list. Static constructors may not have an argument list.

We can now use this class in our Modify Finding rule in a way that is both more convenient and performant:

// Mark topics where expression contains sensitive word
if (Sensitive2.Is(f.e)) {
  f.t = "Sensitive: " + f.t;
}

Note that we no longer create and instance of the class, but simply invoke the Is method on the class itself. The performance advantage comes from the fact that the static constructor will be called only once, regardless of how many times the rule is invoked.

Spelling Correction Example

We can build on this approach by creating a more complete example of a class that corrects many common English misspellings. Here is a class to correct English misspellings:

// English spelling corrections
class English {
  private static var taxonomy: Taxonomy;

  private static function Add(groupSynonyms: String[]) {
    if (groupSynonyms.length > 1) {
      var group: Group = taxonomy.AddGroup(groupSynonyms[0]);
      for (var i: int = 1; i < groupSynonyms.length; i++) {
        group.AddRegex("<" + groupSynonyms[i] + ">");
      }
    }
  }

  public static function Correct(t: String) {
    return taxonomy.Replace(t);
  }

  // static constructor
  static English {
    taxonomy = new Taxonomy();

    Add(["a lot", "alot"]);
    Add(["about a", "abouta"]);
    Add(["about it", "aboutit"]);
    // ... many more such lines ...
    Add(["your", "yuor"]);
  }

}

This implementation also demonstrates the use of the public and private keywords to permit or deny outside access to the members of the class.
We can use our new class in a Modify Response on Load rule very simply:

f.r = English.Correct(f.r);

Testing Class Rules

Testing Class rules requires a bit more work than testing other rule types. This is because your Class rules do not necessarily operate on a single Finding like the other rule types do. So you need to add your own test code to test Class rules.  You can add test code using this template:

@if(@test)
class Tester {

  function test(f: Finding) {
    // your test code here...
    return f; // You may also return an array of Finding objects
  }

}
@end

The conditional compilation @test is set to true by the Rule Set compiler when testing rules in the rule edit dialog. It is set to false when the rule is actually used in an Inspection.
The test code above will therefore be compiled only when testing the rule.
When testing a Class rule Ascribe searched for a class named Tester with a method named test that accepts a Finding argument. If found it invokes the method with a Finding with properties as specified in the dialog. The Finding returned by the test method is displayed in the test results section of the dialog.
Here is an implementation of the Sensitive2 class in our previous example, with test code:

class Sensitive2 {
  static var taxonomy = new Taxonomy();

  // class constructor
  static Sensitive2 {
    taxonomy.AddGroupAndSynonyms("", "<sue>", "<lawyer>", "<suspend>", "<cancel>");
  }

  // Return true if text contains a sensitive word
  static function Is(text: String) {
    return taxonomy.IsMatch(text);
  }
}

@if(@test)
class Tester {

  function test(f: Finding) {
    if (Sensitive2.Is(f.e)) {
      f.t = "Sensitive: " + f.t;
    }
    return f;
  }

}
@end

Sharing Class Rules Across Rule Sets

If we have developed an elaborate class such as our spelling corrector, we may well want to use it in more than one Rule Set. We can do so by placing the Class rule in a Rule Set with ID Global Classes. This ID is special and indicates that this Rule Set contains one or more Class rules that should be available to all Rule Sets. We recommend adopting a naming convention for classes defined in these Class rules, such as prefixing the word “Global” to each class name. This helps make it clear in rules that use these shared Class rules that they are defined in the Global Classes Rule Set.

When using a Global Classes Rule Set be aware of certain pitfalls:

  • The classes defined in the Global Classes Rule Set are imported into other Rule Sets only if the Global Classes Rule Set is enabled and not marked for deletion.
  • You can easily break all your Rule Sets that use the shared classes by deleting or disabling the Global Classes Rule Set, or by introducing bad code into a class in this Rule Set.

Because problems in the Global Classes Rule Set will be shared by all Rule Sets that use it, we recommend carefully testing your class definitions in a separate Rule Set before adding the tested code to the Global Classes Rule Set.

Summary

Class rules can be used for code reuse and performance improvement in your Rule Sets. Although not covered in this article they also allow for introduction of user defined data types. There is a bit to learn about how to create Class rules, but the small investment of time will pay off if you plan to develop a significant collection of Rule Sets.

Leave a Reply

Your email address will not be published. Required fields are marked *