Native Functions
A native function is a Java method that is exposed as a function at the script level. Every native function has the same Java signature:
Object _myFunction(Joe joe, Args args) { ... }
- Every native function returns a Joe value, possibly
null
. - Every native function receives two arguments:
joe
, a reference to theJoe
interpreter, andargs
, any arguments passed to the function.
It is the function's task to do the following:
- Arity Checking: Ensure that it has received the correct number of arguments.
- Argument Conversion: Ensure that those arguments have the desired Java data types, and throw meaningful errors if they do not.
- Do the required computation—which often involves making a simple call to some Java API.
- Return the Result: If any.
Joe provides the tools to make each of these steps simple and concise.
For example, a simple function to square a number would look like this:
Object _square(Joe joe, Args args) {
args.exactArity(1, "square(x)"); // Arity check
var x = joe.toDouble(args.next()); // Argument conversion
return x*x; // Computation and return
}
Installing a Native Function
To install a native function into a Joe interpreter, use the
Joe::installGlobalFunction
method:
var joe = new Joe();
...
joe.installGlobalFunction("square", this::_square);
Arity Checking
A function's arity is the number of arguments it takes. Joe provides four methods for checking that a native function has received the correct number of arguments. There are several cases:
Exact Arity: the Args::exactArity
method checks that the function has
received exactly the required number of arguments, as shown in the _square()
example above. It takes two arguments: the required number, and the
function's signature:
args.exactArity(1, "square(x)");
If args contains an incorrect number of arguments,
exactArity()
will throw a JoeError
:
Wrong number of arguments, expected: square(x).
Minimum Arity: the Args::minArity
method checks that the
args contains at least a minimum number of arguments. For example,
Number.max()
requires at least 1 argument but can take any number of
arguments.
args.minArity(1, "Number.max(number,...)");
Arity Range: the Args::arityRange
method checks that the number of
arguments false within a certain range. For example, the
String
type's
substring()
method takes
1 or 2 arguments:
args.arityRange(1, 2, "substring(beginIndex, [endIndex])");
More Complex Cases: in rare cases the function will examine the arguments
more closely and throw an arityFailure
explicitly if some required pattern
isn't found.
if (args.size() > 7) {
throw Args.arityFailure("myFunc(a, b, c, ...)");
}
Argument Conversion
Before passing an Object
to a Java method it's necessary to cast it or
convert it to the required type. Joe catches unexpected Java errors in
native functions, but it's better to check explicitly and to produce an
appropriate error message if a value has the wrong type. Joe provides a
family of argument conversion methods for this purpose, and client-specific
converters are easily implemented.
For example,
var x = joe.toDouble(args.next());
args.next()
pulls the next unprocessed argument from the args queue.joe.toDouble()
verifies that it's aDouble
and returns it as adouble
, or throws aJoeError
if it is not.
The JoeError
message will look like this:
Expected number, got: <actualType> '<actualValue>'.
See:
- The
Joe
class's Javadoc for the family of converters - The
Joe
class's source code for how they are implemented - The many
ProxyTypes
incom.wjduquette.joe.types
for examples of their use.
Converting Strings
There are three ways to convert an argument that is to be treated as a String.
joe.toString(arg)
requires that the value is already a JavaString
.joe.stringify(arg)
will convert the argument, of whatever type, to its Joe-specific string representation. Proxy types can customize that representation.arg.toString()
will convert the argument to its default Java string representation. Usually it's better to usejoe.stringify(arg)
.
Which of these you use will depend on the native function in question. To some extent it's a matter of taste.
Converting Booleans
Joe allows any value to appear in Boolean expressions. false
and null
are interpreted as false; all other values are interpreted as true. As a result,
it's best to convert arguments meant to be used as Booleans with the
Joe.isTruthy()
function; this guarantees that the native function handles
boolean values in the same way as Joe does at the script level:
var flag = Joe.isTruthy(arg);
Returning the Result
If the function is called for its side effects and has no real result, it
must return null
.
Any other value can be returned as is, but the following rules will make life easier:
Returning Integers: cast integer results to double
. Joe understands
supports doubles directly, but integers are an opaque type.
return (double)text.indexOf("abc"); // Convert integer index to double
Returning Lists: Most lists should be returned as ListValue
values,
which can be modified freely at the script level.
If Java code produces a list that need not or must not be modified, then it can be made read-only:
return joe.readonlyList(myList);
If the Java list is not a List<Object>
, and needs to be modified in place
at the script level, wrap it and specify the item type. (This is
often necessary in JavaFX-related code):
return joe.wrapList(myList, MyItemType.class);
Joe will then ensure that only values assignable to MyItemType
are
added to the list. There are several variants of Joe::wrapList
.
Other Collections: Joe supports maps and sets in much the same way as lists.