Developing Function Classes

Each function defined by the Calculation plug-in must implement the interface com.engineous.sdk.calc.Function.

You implement the com.engineous.sdk.calc.Function interface by extending one of the following abstract classes:

  • AbstractDoubleFunction. A function that takes one or more real numbers as arguments and returns a real number. If the function is called with an array as an argument, the function is called for each element of the array and an array of the results is returned.

  • AbstractScalarFunction. A function that accepts scalar values as arguments and returns a single scalar value as the result. The arguments and result can be any Isight data type. If such a function is called with an array as an argument, the function is applied separately to each element of the array and an array of the results is returned.

  • AbstractFunction. A function, which is the most general form, that accepts zero or more arguments that are scalars, arrays, or even aggregates and returns a scalar, array, or aggregate of any Isight data type. This function is the only class that can do special processing for array arguments or return an array result of a different size than the arguments. This function is also the most complex to implement.

These abstract classes provide additional functionality to provide a description, data type information, and a function call template for the Calculation editor to use.

All functions must have a name. Except for functions used as operators, the name must start with a letter and continue with only letters, digits, and underscores. A function whose name contains punctuation or spaces cannot be called. Functions can have the same name as a parameter—a function name is distinguished as being followed by open parentheses in the text of a Calculation expression.

These classes and all other classes described in this section are described in the Isight API javadocs. You can find the classes in the main.html file in the <Isight_install_directory>/Docs/doc/api/_index directory.

Any class that implements a function must be coded so that it can be reentered, since the same function object can be used simultaneously from multiple instances of the Calculation engine on different threads. Therefore, all working storage must be held in local variables of the eval() method—class member variables can be used only for static information such as the function name or the number of arguments. If a complex function requires a large data structure during evaluation, allocate the data structure within the eval() method and store it only in local variables. You can then pass the data structure to any utility methods inside the function class.

If a function will be implemented using an existing Java class, an instance of that class should be created in the eval() method, called to calculate the result, and then discarded. Any attempt to reuse the class instance will likely cause problems when several instances of the Calculator run in parallel. This prohibition does not apply if the existing class is called only through static methods.

Extending AbstractDoubleFunction

To implement a function that takes only real numbers as arguments and returns a real number as the result, extend class com.engineous.sdk.calc.AbstractDoubleFunction. The only function that needs to be implemented is double eval(double[]). The standard form if the function is defined as a separate class is:

import com.engineous.sdk.calc.*;
class Sind extends AbstractDoubleFunction {
    public Sind() { super("sind", 1); }
    public double eval(double[] arg) throws CalcException {
       return Math.sin(arg[0] * Math.PI / 180.0);
    }
}

The call to the super-class constructor is required. The first argument is the name of the function as used in the Calculation engine. The second argument is the number of arguments the function expects. Zero or a positive number means that exactly that number of arguments is required. A negative number indicates that the function takes a variable number of arguments and the absolute value is the minimum number of arguments. That is, '2' means 'exactly 2 arguments' but,'-2' means '2 or more arguments'. The number of arguments passed to evaluate will always be within the bounds of the second argument to the constructor.

If the function requires only certain patterns of arguments (e.g., 1 or 2 arguments but never more than 2), override the method isValidNargs(int) to return true for a valid number of arguments and false for an invalid number. In the constructor, set the number of arguments to the minimum valid number of arguments (1 in this case). The default implementation of isValidNargs handles the number of arguments from the constructor, as described above. isValidNargs can be overridden for AbstractScalarFunction or AbstractFunction in the same way.

The eval method may optionally throw a CalcException to indicate an error. It can also throw unchecked exceptions such as ArithmeticException or IndexOutOfBoundsException, and the Calculation engine will correctly report them as a function failure.

A function can be defined as an anonymous inner class as follows:

Function argument = new AbstractDoubleFunction("argument", 2) {
    public double eval(double[] arg) {
        double re = arg[0];
        double im = arg[1];
        return Math.sqrt(re * re + im * im);
}};

The following is a simple example of a function that accepts a variable number of arguments. It calculates the product of all its arguments:

Function prod = new AbstractDoubleFunction("product", -1) {
    public double eval(double[] arg) {
        double result = arg[0];
        for (int i = 1; i < arg.length; i++) {
            result *= arg[i];
        } 
        return result;
}};

Extending AbstractScalar Function

Instead of directly passing the arguments to the function, the eval method of AbstractScalarFunction receives a ScalarEnv object that gives access to the arguments. The ScalarEnv is also used to return the value of the function.

You can use methods of the ScalarEnv object to do the following:

  • Get the number of arguments: getNargs().

  • Get the type of an argument: getArgType(i).

  • Retrieve the value of an argument as a specific data type: getAsBool(i), getAsInt(i), getAsReal(i), and getAsString(i).

  • Set the return value. One of these must be called exactly once: setResult(boolean), setResult(int), setResult(double), setResult(String)

  • Handle arguments as ScalarVariables: getScalarArg(i), setResult(ScalarVariable), createScalar(type).

The following is an example of the eval method for a function that concatenates two or more strings:

public void eval(ScalarEnv env) throws CalcException {
   StringBuilder buf = new StringBuilder();
   for (int i = 0; i < env.getNargs(); i++) {
      buf.append(env.getAsString(i));
   }
   env.setResult(buf.toString());
}

It is not necessary to do an explicit check of the argument number or type if there is only one reasonable number or type; you can retrieve the arguments you expect to be there using the data type you expect. If the actual arguments are wrong, the ScalarEnv methods will throw a CalcException with a useful error message. Arguments of the wrong type will be converted to the correct type if possible. Any type of argument can be converted to a string; Boolean and integer arguments can be converted to real. In addition, a string argument that contains only digits (and period, minus, and 'E' for real) will be converted to an integer or real.

The constructor for a class derived from AbstractScalarFunction must set the function name and number of arguments by calling the super constructor, exactly as for AbstractDoubleFunction. It can also define the expected argument types and result type and provide a description of the function for the Functions menu in the Calculator editor. For example, the following is a possible declaration of the above function to concatenate strings, with explicit declarations that the arguments must be type string and the result will be a string:

import com.engineous.sdk.calc.*;
import com.engineous.sdk.vars.EsiTypes;
public class Concat extends AbstractScalarFunction {
   public Concat() {
      super("concat", -2);
      argType = EsiTypes.STRING;
      resultType = EsiTypes.STRING;
      description = "Concatenate two or more strings into one 
      string.";
   }
   public void eval(ScalarEnv env) throws CalcException {
      StringBuilder buf = new StringBuilder();
      for (int i = 0; i < env.getNargs(); i++) {
         buf.append(env.getAsString(i));
      }
      env.setResult(buf.toString());
   }
}

See the JavaDocs for class com.engineous.sdk.calc.AbstractFunction for a description of the class attributes argType, resultType, and description. A class derived from AbstractScalarFunction can also override the method getArgType to provide type information when different arguments have different expected data types.

To view an example of a function that takes one string argument and two integer arguments, see the inner class Substr in the following directory: <Isight_install_directory>\<operating_system>\examples\development\plugins\calculation

Extending AbstractFunction

The abstract class AbstractFunction should be extended when implementing a function that provides special processing for array arguments or that returns an array for scalar arguments. As with AbstractScalarFunction, the function arguments are not passed directly to eval; instead, a FunctionEnv argument is passed that allows access to the arguments. FunctionEnv has methods to get the number of arguments, to retrieve and evaluate an argument, and to create variable objects that can be returned as the value of the function.

An argument to your function is evaluated each time getArgument(int) is called. Evaluating an argument is rarely useful, but it can be used to stop the process of evaluating the argument; for example, if evaluating some arguments produces a result that makes it unnecessary to evaluate the other arguments. The 'if' function uses this method to evaluate only one of the second or third arguments, based on whether the first argument is true or not. For most uses, you should not call getArgument(int) more than once for a given argument number.

The Calculation example has two functions that use AbstractFunction to process or return an array. To view an example, refer to the inner classes Join and Split in the following file: <Isight_install_directory>\<operating_system>\examples\development\plugins\calculation\StringFunc.java

The method FunctionEnv.evalScalar(ScalarFunction) can be used to shift from the general evaluation of a function with arguments of unknown structure to the usual rules for handling scalar functions, where array arguments cause the function to be evaluated separately for each array element and an array of the results are returned. All the statistical functions use this process: If they are called with a single argument, which must be an array, the function is evaluated on that array and a scalar result is returned. Otherwise, processing is passed off to FunctionEnv.evalScalar() to evaluate the statistic over all the arguments or over each element of an array argument. Typically, you cannot use this process because you have to decide whether to call evalScalar before any of the arguments are evaluated.

The following is part of the code for the mean function that shows how to evaluate using arrays or scalars as appropriate:

public class Mean extends AbstractFunction implements 
ScalarFunction {
   public Mean() {
      super("mean", -1);   // note 1 or more argument allowed
      resultType = EsiTypes.REAL;   // result MUST be real
      // Note that argument types are not given - anything will 
      // be converted to a Real before processing (if possible)
   }
// override from AbstractFunction to give different arg. 
structures
   public int int getArgStructure(int argNo, int nArgs) {
      if (nArgs == 1) {
         // one argument MUST be an array
         return Variable.STRUCT_ARRAY;
      } else {
      // otherwise use default (which is Scalar or Array)
         return super.getArgStructure(argNo, nArgs);
      }
   }
   public Variable eval(FunctionEnv env) {
      if (env.getNargs() == 1) {
         return evalMean(VariableUtil.getArrayAsReal1d(
         (ArrayVariable)env.getArgument(0));
      }
      else {
         return env.evalScalar(this);
      }
   }
// implement ScalarFunction interface
// pack all arguments into a double array and then calculate 
result.
   public void eval(ScalarEnv senv) {
      double[] args = new double[senv.getNargs()];
      for (int i = 0; i < senv.getNargs(); i++) {
         args[i] = senv.getAsReal(i);
      }
      senv.setResult(evalMean(args));
   }
// Actual implementation of mean
   private double evalMean(double[] args) {
      double sum = 0.0;
      for (int i = 0; i < args.length; i++) {
         sum += args[i];
      }
      return sum / args.length;
   }
}

Implementing Function Directly

On occasion, you may need to implement the Function interface directly. Only three methods need to be defined: getName(), isValidNargs(int), and eval(FunctionEnv). None of the extra processing for argument number, type, structure, or description provided by the Abstract*Function classes is available when implementing Function directly.

Implementing the CalculationPlugin Interface

To allow a Calculation plug-in to define multiple functions or operators, the implementation class of the plug-in must implement the CalculationPlugin interface. The following shows the basic structure of a CalculationPlugin implementation:

import com.engineous.sdk.calc.*;
import java.util.*;
public class StringFunc implements CalculationPlugin {
   public Collection<Function> getFunctions() {
      ArrayList<Function> func = new ArrayList<Function>();
      func.add(new MyFunc1());
      func.add(new MyFunc2());
      return func;
   }
   public Collection<Operator> getOperators() {
      ArrayList<Operator> op = new ArrayList<Operator>();
      op.add(new Operator(Operator.OpType.INFIX,
      Operator.OpAssoc.LEFT, 10, "&", new MyFunc3(), null);
      return op;
   }
}

That is, you build a collection of function objects and return it from getFunctions() and build a similar list of operators and return them from getOperators(). If there are no functions nor operators, return either an empty collection or null.

Operators are always built with the constructor for the class operator—never sub-class this class. An operator is a description of what symbol is used for the operator and where it can be used.

Note: See the JavaDocs for the operator class for the arguments to the operator constructor and the enum values that can be passed to some of the arguments.

Each operator object has a function object that implements the operator. The number of arguments must match how the operator is used—one argument for a unary prefix operator, two arguments for a binary infix operator. The function name does not matter—it can be the operator symbol, or it can be a proper function name that can be called as a function. The symbol for an operator can be any Unicode character string as long as it starts with a character that is not a letter or digit and it is not one of the reserved characters: comma, semicolon, parentheses, and square brackets.

While the enum class Operator.OpType has six distinct values, only two of the values are currently usable for user-defined functions: OpType.PREFIX and OpType.INFIX. The others are for future expansion.

The example file in <Isight_install_directory>\<operating_system>\examples\development\plugins\calculation\StringFunc.java is more complex because it builds the collections once on the first call and returns the same collection for each subsequent call. The example is a minor optimization that is only advantageous for very large function collections or one that is used many times in the same model.