Developing Operators
Operators are the functions of GAML. An operator takes one or more operands (expressions) and returns a value: sqrt(4.0), {1, 2} + 4.5, my_matrix row_at 2, [1, 2, 3] count (each > 1). To add an operator, annotate a Java method with @operator. The GAMA annotation processor (gama.processor) then registers it in the plugin's generated gaml/ folder (package gaml.additions.<shortcut>, class GamlAdditions). Never edit that generated code by hand.
This page shows how to write operators. For the full list of attributes of each annotation, see the index of annotations. For workspace setup, see Installing the development version; for plugin setup, see Developing Plugins.
Static and non-static operators​
You can declare an operator in two ways.
Static methods​
Most operators are public static methods grouped in "operators" classes, for example gama.core/src/gama/gaml/operators/Maths.java:
@operator (
value = "sqrt",
can_be_const = true,
category = { IOperatorCategory.ARITHMETIC },
concept = {})
@doc (
value = "Returns the square root of the float operand.",
returns = "a non-negative {@code float}.",
special_cases = { "sqrt(0.0) = 0.0", "If the operand is negative, a runtime exception is raised." },
examples = { @example (value = "sqrt(4.0)", equals = "2.0"),
@example (value = "sqrt(0.0)", equals = "0.0") })
@tests ({ @test ("sqrt(4.0) = 2.0"),
@test ("is_error(sqrt(-1.0))"),
@test ("sqrt(#infinity) = #infinity") })
public static Double sqrt(final IScope scope, final Double v) throws GamaRuntimeException {
if (v < 0) {
GAMA.reportAndThrowIfNeeded(scope,
GamaRuntimeException.error("The sqrt operator cannot accept negative inputs", scope), true);
}
return Math.sqrt(v);
}
The operands are the parameters of the method, in order. With one parameter the operator is unary (sqrt(4.0)). With two parameters it can also be used as a binary infix operator (a op b). With more parameters you call it as a function (op(a, b, c)).
Non-static methods​
You can also put @operator on an instance method, including a default method of an interface. The object that declares the method becomes the first operand, and the method's parameters become the next operands. For example, in gama.api/src/gama/api/types/geometry/IPoint.java:
@operator (
value = IKeyword.PLUS,
can_be_const = true,
category = IOperatorCategory.POINT,
concept = {})
@doc (
value = "Returns a point with coordinate summing of the two operands.",
usages = @usage (
value = "if the left-hand operand is a point and the right-hand a number, returns a new point ...",
examples = { @example (value = "{1, 2} + 4.5", equals = "{5.5, 6.5,4.5}") }))
default IPoint plus(final double d) {
return plus(d, d, d);
}
In GAML this is the binary operator point + float: the point is the receiver (this), and d is the second operand. In the same way, IMatrix.getRow(Integer num_line) (in gama.api/src/gama/api/types/matrix/IMatrix.java) defines matrix row_at int.
Use non-static operators when the operation belongs naturally to a value class (geometry, matrix, colour, your own type...).
The IScope parameter​
If the first Java parameter is an IScope, GAMA injects the current scope when the operator runs. The scope is not an operand. sqrt(final IScope scope, final Double v) is a unary operator, and a non-static method foo(IScope scope, Integer i) is a binary operator (receiver foo i). The processor only detects the scope as the first parameter. Use it when you need random numbers, the simulation, the agent, the topology, or when you raise errors. See The IScope interface.
Rules checked by the processor​
gama.processor/src/gama/processor/elements/OperatorProcessor.java checks every @operator method and reports problems in the Eclipse Problems view.
Errors
- The method must be
public. - A static operator needs at least one operand, not counting the
IScope("an operator needs to have at least one operand"). - The method must return a value.
voidis rejected ("operators need to return a value"). - Java arrays are not allowed as parameters or return types. Wrap them in a GAML container (
IListorIMatrix). - Operators cannot return Java executables.
Warnings
char,byteandshortparameters or return values are cast toint.- Prefer GAML types to plain Java types:
IListinstead ofList/ArrayList,IMapinstead ofMap,Integerinstead oflong/short,Doubleinstead offloat, and the GAMA colour type instead ofjava.awt.Color. - "operator 'x' is not tested" is reported unless the operator is
internal, is annotated with@no_test,@testor@tests, is deprecated, or has at least one executable example in its@doc(including examples inside@usage). - "documentation missing" is reported when there is no
@doc.
Main attributes of @operator​
| Attribute | Default | Use |
|---|---|---|
value | (required) | The name(s) of the operator. You can give several names, e.g. { "copy_between", "between" }. |
category | {} | Categories used to sort the documentation. Use the constants of IOperatorCategory (e.g. ARITHMETIC, CONTAINER, POINT, MATRIX, STRING, SPATIAL...). |
concept | {} | Keywords from IConcept used by the search in the documentation. |
can_be_const | false | true if the result only depends on the operands. If all operands are constants, the compiler then computes the result once at compile time. Do not use it for operators that use randomness, time, files or the state of agents. |
type | ITypeProvider.NONE | The GAML return type, when the Java return type is not precise enough (e.g. Object). |
content_type | ITypeProvider.NONE | The content type of the returned container. |
index_type | ITypeProvider.NONE | The index (key) type of the returned container. |
content_type_content_type | ITypeProvider.NONE | The content type of the contents, for containers of containers (e.g. rows_list on a matrix). |
expected_content_type | {} | The types accepted as content of the operands (e.g. IType.BOOL for the filter of count). |
iterator | false | true if the right operand is evaluated once for each element with the pseudo-variable each. |
internal | false | Hides the operator from the documentation and the completion. |
doc | {} | You can put the @doc here, or annotate the method directly (more usual). |
See @operator in the index for all details.
Typing the result​
The type values are either IType ids (IType.INT, IType.GEOMETRY...) or the constants of ITypeProvider, which compute the type from the operands at compile time. For the ..._AT_INDEX constants, add the 1-based index of the operand:
ITypeProvider.TYPE_AT_INDEX + 1: the type of the first operand.ITypeProvider.CONTENT_TYPE_AT_INDEX + 1: the content type of the first operand.ITypeProvider.KEY_TYPE_AT_INDEX + 2: the key type of the second operand.ITypeProvider.DENOTED_TYPE_AT_INDEX + 2: the type denoted by the second operand (e.g.speciesortypeoperands).ITypeProvider.BOTH: the common supertype of the operands.ITypeProvider.FLOAT_IN_CASE_OF_INT:floatwhen the result would beint.
The old constants FIRST_TYPE, FIRST_CONTENT_TYPE, SECOND_TYPE, etc. are deprecated. For a non-static operator, the receiver is operand 1. Example (IMatrix.java):
@operator (
value = "row_at",
content_type = ITypeProvider.CONTENT_TYPE_AT_INDEX + 1,
can_be_const = true,
category = { IOperatorCategory.MATRIX },
concept = { IConcept.MATRIX })
@doc (value = "returns the row at a num_line (right-hand operand)", ...)
IList<T> getRow(Integer num_line);
matrix<int> m; m row_at 0 is therefore typed list<int>.
Iterator operators​
Operators such as count, where, collect or group_by evaluate their right operand once for each element. Declare them with iterator = true. The method receives the name of the iteration variable, the container, and the right operand as an unevaluated IExpression (gama.core/src/gama/gaml/operators/Containers.java):
@operator (
value = { "count" },
iterator = true,
expected_content_type = IType.BOOL,
category = IOperatorCategory.CONTAINER,
concept = { IConcept.CONTAINER })
@doc (value = "returns an int, equal to the number of elements of the left-hand operand that make the right-hand operand evaluate to true.", ...)
public static Integer count(final IScope scope, final String eachName, final IContainer original,
final IExpression filter) { ... }
Look at the existing iterators in Containers.java before you write one.
Documentation and tests​
Every operator should have a @doc. The documentation of GAML is generated from it (see Documentation).
value: the description.returns,comment,special_cases,seeanddeprecatedcomplete it.usages: one@usagefor each variant, often used when several Java methods share the same name. SetmasterDoc = trueon the@docof the overload that holds the main description.examples:@exampleelements. An example withequals(orisNot,raises) becomes a test. UseisExecutable = falsefor code that cannot be run alone,test = falseto show a result without checking it, andisTestOnly = truefor an example that is a test only and not shown in the documentation.
You can also write tests directly in GAML with @test, repeated or grouped in @tests. Each value becomes an assert statement (several statements can be separated by ;; if the string ends with ;, no assert is added). If an operator cannot be tested, add @no_test(Reason.IMPOSSIBLE_TO_TEST) (other reasons: ALREADY_TESTED, DEPRECATED, INTERNAL).
The processor writes these tests into gaml/models/Tests/Generated From <Plugin>/models/<Plugin> Tests.gaml in your plugin (e.g. Core Tests.gaml for gama.core). In the same folder, it also generates fuzz tests, one file per Java class (e.g. Maths Fuzz Tests.gaml). These tests call every operator with edge values (0, -1, infinities, NaN, empty containers, nil) and check that GAMA does not crash. Exclude operators that act on the outside world (files, network, user interface) or that can hang with @no_fuzz_test("reason"), as in gama.core/src/gama/gaml/operators/System.java:
@no_fuzz_test ("acts on the outside world (files, network, clipboard, user interface, shell...)")
A complete extension example is gama.extension.dataframe/src/gama/extension/dataframe/DataFrameOperators.java. It has operators returning a custom type (type = IDataframeConstants.ID), @tests, and internal operators marked with @no_test.
Creating an operator with the developer tools​
The GAMA developer tools add wizards to Eclipse. Choose GAMA > Create GAMA artefacts > New GAMA Operator (or File > New > Other... > GAMA Development > New GAMA Operator). The wizard "creates a class defining GAML operators, with documentation and examples used as tests". It asks for:
- the GAMA plugin,
- the package,
- the class name (default
MyOperators), - the operator name.
It generates a class with two static overloads (for Integer and Double) that return twice their operand. Each one has @operator(can_be_const = true, category = IOperatorCategory.ARITHMETIC, concept = IConcept.MATH) and a @doc with examples. Replace the body and the documentation with your own.
The developer tools also help you complete existing operators:
- Quick fixes (Ctrl+1 / Cmd+1) on the processor warnings. On "documentation missing", add a
@docskeleton. On "operator '...' is not tested", add a failing@test("false /* TODO ... */"), add@no_test(Reason.ALREADY_TESTED)or@no_test(Reason.IMPOSSIBLE_TO_TEST), or generate a GAML test stub. - GAMA > Report > Missing annotations lists the undocumented elements and untested operators of each plugin.
- GAMA > Repair GAMA plugins > Generate missing tests writes
models/Tests/Untested Operators.gamlin the selected plugins, with oneteststub for each untested operator. - Search GAML keyword (Ctrl+Shift+Alt+K / Cmd+Shift+Alt+K) finds the Java code of an existing operator. Use the prefix
op:, e.g.op:row_at.
See also Developing Types, Developing Skills and Developing Statements.