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, 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 a
specific number of arguments, as shown in the _square()
example above. It
takes two arguments:
args.exactArity(1, "square(x)");
- The number of arguments expected.
- A string representing the function's signature.
If args doesn't contain exactly the correct number of arguments,
exactArity()
will use Args.arityFailure()
to throw a JoeError
with this message:
Wrong number of arguments, expected: square(x).
Minimum Arity and Arity Ranges
Similarly, 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,...)");
And 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, it's simplest for the native function to access the
Args
's size directly, and throw an arityFailure
explicitly:
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, and to produce an appropriate error message
if that cannot be done. 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
.
The JoeError
message will look like this:
Expected number, got: <actualType> '<actualValue>'.
See the Joe
class's Javadoc for the family of converters, and the
Joe
class's source code for how they are implemented; and the many
ProxyTypes
in com.wjduquette.joe.types
for examples of their use.
Converting Strings
There are two 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 string representation. Proxy types can customize that representation.
Which of these you use will depend on the native function in question. To some extent it's a matter of taste.
Converting Booleans
In Joe, any value can be used in a Boolean expression. false
and null
are interpreted as false; any other value is 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
doubles, 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.