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.