Custom Java Types / Native Functions
This chapter is about custom types written in Java. See Menter Types for more information about types natively written in Menter.
Types of Types
You can extend the functionality of Menter in different ways:
Native functions
Custom Java Type
Both of these are registered statically via the EvaluationContext
class. This means that you can't add new types or native functions per interpreter instance. This is a limitation of the current implementation, but as I see it, this is not too big of a problem.
More on validating the arguments passed to native functions and custom types can be found at the end of this chapter.
When to use Native Functions
Native functions are ideal for operations that manipulate primitive values, such as mathematical functions, string manipulation, and date/time handling.
If you need to interact with external systems or APIs, you may want to use a native function to access those resources directly.
If your operation is relatively simple and only needs a few lines of code.
When to use Custom Java Types
If you need to implement complex data structures or algorithms.
If you want to provide an object-oriented interface for working with data.
If you want to provide a domain-specific language (DSL) for your application, custom types can be used to define the syntax and semantics of your language.
Native functions
Native functions are functions written in Java that can be called from Menter. Creating one consists of two steps:
Register your function in the EvaluationContext
class. For the function, you can choose between two different signatures, where one contains significantly more (powerful) information about the context in which the function is.
Create a context with that native function and exports it from the module
1. Register your function
The two signatures are:
Value execute(List<Value> parameters)
Value execute(GlobalContext context, EvaluationContextLocalInformation localInformation, List<Value> parameters)
Both of them receive the parameters the function was called with as a list of Value
objects. The second one also receives the GlobalContext
and EvaluationContextLocalInformation
objects. More on these in a different chapter.
When registering the function, you have to specify the exact context name the native function definition will be in and the actual function name.
EvaluationContext.registerNativeFunction("contextName", "functionName", parameters -> {
return parameters.get(0);
});
2. Create a context with that native function
Now you can create a context with that native function:
interpreter.evaluateInContextOf("contextName", "native functionName()");
interpreter.evaluateInContextOf("contextName", "functionName(1)"); // 1 (number)
To access them from other contexts, you can obviously export and import them.
How I usually do it
When you look at the source code of Menter, you will see the following pattern for the core native functions:
I will now demonstrate this with some of the core modules:
public abstract class CoreModuleMath {
static {
EvaluationContext.registerNativeFunction("math.mtr", "sin", CoreModuleMath::sin);
EvaluationContext.registerNativeFunction("math.mtr", "cos", CoreModuleMath::cos);
EvaluationContext.registerNativeFunction("math.mtr", "tan", CoreModuleMath::tan);
...
}
// short signature
private static Value floor(List<Value> values) {
return applySingleValueFunction("floor", values, v -> v.setScale(0, RoundingMode.FLOOR));
}
// example from the reflection module that uses the long signature
public static Value setVariable(GlobalContext context, EvaluationContextLocalInformation localInformation, List<Value> parameters) {
return anyAnyAction(parameters, "setVariable", () -> {
context.addVariable(parameters.get(0).toDisplayString(), parameters.get(1));
return Value.empty();
});
}
}
Now, before adding the native functions to my interpreter, I load all the core modules:
static {
try {
Class.forName("de.yanwittmann.menter.interpreter.core.CoreModuleIo");
Class.forName("de.yanwittmann.menter.interpreter.core.CoreModuleSystem");
Class.forName("de.yanwittmann.menter.interpreter.core.CoreModuleDebug");
Class.forName("de.yanwittmann.menter.interpreter.core.CoreModuleMath");
Class.forName("de.yanwittmann.menter.interpreter.core.CoreModuleReflection");
Class.forName("de.yanwittmann.menter.interpreter.core.CoreModuleCmdPlot");
} catch (ClassNotFoundException e) {
LOG.error("Failed to load core module", e);
}
}
after which I finally load the files that contain the native functions:
private final static String[] MENTER_SOURCE_FILES = {
"io.mtr",
"system.mtr",
"math.mtr",
"debug.mtr",
"reflect.mtr",
"cmdplot.mtr",
};
private void loadMenterCoreFiles() {
try {
for (String file : MENTER_SOURCE_FILES) {
loadContext(readLinesFromResource("/src/" + file), file);
}
finishLoadingContexts();
} catch (Exception e) {
throw new MenterExecutionException("Failed to load Menter core files", e);
}
}
The contents of such a file can be found in the documentation of each core module, example with the math.mtr
file.
Custom Java Type
Custom Java types are classes that extend the CustomType
class and have the @TypeMetaData
annotation. In contrast to native functions, these actually represent Value
types in Menter, meaning that you truly create instances of your type in Menter using the new
keyword and call Java functions on them.
Internally, this is solved using reflection.
@TypeMetaData
The @TypeMetaData
annotation is used to specify the name of the type in Menter and the name of the module it is in.
@TypeMetaData(typeName = "TypeName", moduleName = "moduleName")
If multiple types are registered in the same module, they will be accessible via that one module, as the module name is unique and all types are merged into one module.
@TypeFunction
In contrast to native functions, there is only one signature available:
Value functionName(List<Value> parameters)
The parameters are the parameters passed to the function call in Menter. Functions, that should be callable inside Menter, have to be annotated with @TypeFunction
. If a static function is annotated with @TypeFunction
, it will be callable using the class name.
@TypeFunction
public Value functionName(List<Value> parameters) {
return parameters.get(0);
}
CustomType
The CustomType
class is the base class for all custom types. It has no abstract methods, as all of them have default implementations. However, you can override them if you want to enable a certain functionality on your type.
// custom truthiness
public boolean isTrue() { return false; }
// enables `for` and `forEach` loops
public Value iterator() { return null; }
// used by functions like `sum` and `avg`
public BigDecimal getNumericValue() { return null; }
// self-explanatory
public String toString() { return "instance of " + getClass().getSimpleName(); }
// currently unused
public int size() { return 0; }
Example
In this case, I think an example speaks a thousand words. Here is a custom type that represents a list of users and another one that represents a user. There are a few things to note, but I will add those at the bottom.
UserList
@TypeMetaData(typeName = "UserList", moduleName = "users")
public class UserList extends CustomType {
private final List<Value> users = new ArrayList<>();
public UserList(List<Value> parameters) {
super(parameters);
}
@TypeFunction
public Value addUser(List<Value> parameters) {
final String[][] parameterCombinations = {
{PrimitiveValueType.STRING.getType(), PrimitiveValueType.NUMBER.getType()},
{"User"}
};
final int parameterCombination = CustomType.checkParameterCombination(parameters, parameterCombinations);
switch (parameterCombination) {
case 0:
final User user = new User(Collections.emptyList());
user.setName(Collections.singletonList(parameters.get(0)));
user.setAge(Collections.singletonList(parameters.get(1)));
users.add(new Value(user));
break;
case 1:
users.add(parameters.get(0));
break;
case -1:
throw invalidParameterCombinationException(getClass().getSimpleName(), "registerUser", parameters, parameterCombinations);
}
return Value.empty();
}
@TypeFunction
public Value getUsers(List<Value> parameters) {
return new Value(users);
}
@Override
public Value iterator() {
return new Value(users.iterator());
}
}
User
@TypeMetaData(typeName = "User", moduleName = "users")
public class User extends CustomType {
private Value name;
private Value age;
public User(List<Value> parameters) {
super(parameters);
final String[][] parameterCombinations = {
{},
{PrimitiveValueType.STRING.getType(), PrimitiveValueType.NUMBER.getType()}
};
final int parameterCombination = CustomType.checkParameterCombination(parameters, parameterCombinations);
switch (parameterCombination) {
case 0:
name = Value.empty();
age = Value.empty();
break;
case 1:
name = parameters.get(0);
age = parameters.get(1);
break;
case -1:
throw invalidParameterCombinationException(getClass().getSimpleName(), "User", parameters, parameterCombinations);
}
}
@TypeFunction
public Value getName(List<Value> parameters) {
return new Value(name);
}
@TypeFunction
public Value setName(List<Value> parameters) {
final String[][] parameterCombinations = {
{},
{PrimitiveValueType.STRING.getType()}
};
final int parameterCombination = CustomType.checkParameterCombination(parameters, parameterCombinations);
switch (parameterCombination) {
case 0:
name = Value.empty();
break;
case 1:
name = parameters.get(0);
break;
case -1:
throw invalidParameterCombinationException(getClass().getSimpleName(), "setName", parameters, parameterCombinations);
}
return Value.empty();
}
@TypeFunction
public Value getAge(List<Value> parameters) {
return new Value(age);
}
@TypeFunction
public Value setAge(List<Value> parameters) {
final String[][] parameterCombinations = {
{},
{PrimitiveValueType.NUMBER.getType()}
};
final int parameterCombination = CustomType.checkParameterCombination(parameters, parameterCombinations);
switch (parameterCombination) {
case 0:
age = Value.empty();
break;
case 1:
age = parameters.get(0);
break;
case -1:
throw invalidParameterCombinationException(getClass().getSimpleName(), "setAge", parameters, parameterCombinations);
}
return Value.empty();
}
@Override
public String toString() {
return "User{" +
"name=" + name.toDisplayString() +
", age=" + age.toDisplayString() +
'}';
}
}
And now we can use them in Menter with all their methods:
MenterInterpreter interpreter = new MenterInterpreter(new Operators());
interpreter.finishLoadingContexts();
EvaluationContext.registerCustomValueType(UserList.class);
EvaluationContext.registerCustomValueType(User.class);
interpreter.evaluateInContextOf("users-context",
"import users inline\n" +
"userList = new UserList()\n" +
"userList.addUser(\"Yan\", 22)\n" +
"userList.addUser(new User(\"Thomas\", 36))");
System.out.println(interpreter.evaluateInContextOf("users-context", "userList.getUsers()").toDisplayString());
interpreter.evaluateInContextOf("users-context", "userList.getUsers().get(1).setName(\"Martin\")");
System.out.println(interpreter.evaluateInContextOf("users-context", "userList.getUsers().get(1).getName()").toDisplayString());
Where the result is:
[INFO] Registered custom type [users.UserList]
[INFO] Registered custom type [users.User]
[User{name=Yan, age=22}, User{name=Thomas, age=36}]
Martin
Now, the things to note:
By importing the users
module inline, both types were imported into the current context, since both were registered in this module.
The UserList.addUser
method allows for passing a User
instance or a String
and a Number
to create a new User
instance.
Checking for parameter combinations can be done using the CustomType.checkParameterCombination
method, which returns the index of the parameter combination that matches the given parameters. If no combination matches, it returns -1
. You can also call the assertAtLeastOneOfParameterCombinationExists
method, which throws an exception if no combination or the invalidParameterCombinationException
method, which creates, but does not throw, an exception.