Extending Intrinsic Classes

You can extend an intrinsic class's programmatic interface by adding new methods and static (class-level) properties.  This capability allows you to add new functionality to an intrinsic class, and then access the new functionality using the same syntax that you would use to access native features of the class.

 

To add a method to an intrinsic class, use the modify statement.  The modify syntax for intrinsic classes is almost exactly the same as it is for regular objects:

 

modify intrinsic_class_name
   method1 ( args ) { method1_code }
   method2 ...
;

 

When you modify an intrinsic class, the intrinsic class must be defined before the modify statement.  In most cases, this simply means that you must include the system header file that defines the intrinsic class before the modify statement and in the same source module.

 

The following example adds a base-2 logarithm function to the BigNumber intrinsic class.

 

#include <bignum.h>

 

modify BigNumber

  log2()

  {

    /*

     *   cache ln(2) – use slightly greater precision than we

     *   actually need, to avoid rounding error

     */

    if (BigNumber.cacheLn2_ == nil

        || BigNumber.cacheLn2_.getPrecision() < getPrecision() + 3)

    {

      BigNumber.cacheLn2_ =

        new BigNumber(2, getPrecision() + 3).logE();

    }

 

    /*

     *   Calculate ln(self), then divide by ln(2) to get the

     *   result (note that ln-base-B of x for any B is equal to

     *   ln(x)/ln(B)).  Reduce the precision of the result back

     *   to our own precision before returning.

     */

    return (self.setPrecision(getPrecision() + 3).logE()

            / BigNumber.cacheLn2_).setPrecision(getPrecision());

  }

 

  // our caches ln(2) value – we don't have any value initially

  cacheLn2_ = nil

;

 

This example illustrates several aspects of intrinsic class extensions.

 

First, note that we can refer to "self" within the method we add to the class.  We're modifying BigNumber, so "self" is always a BigNumber object; we can thus refer to its methods, such as getPrecision() and setPrecision(), and we can also use the value in arithmetic.

 

Second, note that we can add a data property to the class.  In the example, we add a property called cacheLn2, which contains a cached value of the natural logarithm of 2.  (In the example, we have chosen to cache the value rather than recalculate it each time we call the ln2() method as a performance optimization; calculating ln(2) is fairly expensive, so it makes sense to save the value when we first calculate it, and re-use the same value rather than calculating it on each new call to the method.)

 

Note, though, that we can only add class-level properties to the class.  We cannot add instance properties.  In other words, we can only add a property to BigNumber itself, not to each individual BigNumber value.  So, we cannot write something like this within an intrinsic class extension method:

 

  self.val1 = 5;  // ILLEGAL!

 

The statement above is illegal in an intrinsic class extension method because it attempts to store a property value with the object itself, rather than with the intrinsic class.

Restrictions

There are some restrictions on modifying intrinsic classes:

 

Using Aggregation

There might be times when the intrinsic class extension mechanism is too restricted for a particular application you have in mind.  In particular, you might sometimes find it necessary to add new properties to individual instances of a class; since you can't do this by extending an intrinsic class, you will have to find an alternative approach in such cases.

 

One approach that you might consider is aggregation.  Aggregation is a common technique in object-oriented programming in which you create a "wrapper" object that contains an instance of a class you want to extend.  In other words, rather than subclassing, you create a new, independent class, and store an instance of the class you wish to extend as a property of the new class.

 

For example, suppose you wanted to create a complex number class.  (A complex number is a mathematical construct consisting of two components, a "real" part and an "imaginary" part, each of which is an independent real number.)  You can't do this by extending BigNumber, since there's no way to store the two separate numbers making up a complex value.  Instead, you could use aggregation.
 
To create a complex number class using aggregation, we would start with something like this:
 
class Complex: object
  construct(r, i) { r_ = r; i_ = i; }
  r_ = nil // the real part
  i_ = nil // the imaginary part
;
 
Here, the properties r_ and i_ are simply BigNumber values.  We have thus aggregated two BigNumber values into this new class that we call Complex.
 
The biggest disadvantage of using aggregation, especially for something like a mathematical class, is that TADS provides no way of defining operators (such as "+" or "*") on classes.  So, it's impossible to create a Complex class that we can use as though it were an ordinary value in additions or multiplications.  However, we can easily use explicit method calls instead:
 
// adding to Complex's class definition
  add(v) { return new Complex(r_ + v.r_, i_ + v.i_); }
  sub(v) { return new Complex(r_ - v.r_, i_ - v.i_); }
;
 
// perform some arithmetic on some complex values
  local a, b, c;
  a = new Complex(1.0, 2.0);
  b = new Complex(-3.1, -2.0);
  c = a.add(b);
 
It's not quite as syntactically spare as normal arithmetic, but it's not too much worse, and the meaning is clear enough.