Skip to main content

Expressions

An expression computes a value. It appears in conditions, assignments, arguments, return statements, values properties and where clauses. FML expressions are evaluated by Connie, the expression language of the platform: they look like Java expressions, and they mostly behave like them, with a few differences that matter. This page lists the operators, then the differences. The examples come from a model Calc.

Values

An expression works with numbers (7, 1.5), strings ("a"), booleans (true, false), null, and the instances of concepts. A path designates a value by following properties and calls: guest.name, fiction.books.size, this.container.label. The names a path can start from are the local variables, the properties of the concept, and this, container and parameters (see the language tour). Inside a listen, evt is the event.

Operators

KindOperators
arithmetic+ - * / %, and - before a value
comparison== != < <= > >=
logic&& `
conditionalcondition ? a : b
typeinstanceof, and a cast (type) value

Precedence is the usual one, and parentheses group:

public int precedence() {
return 2 + 3 * 4;
}

public int parens() {
return (2 + 3) * 4;
}
assert calc.precedence() == 14;
assert calc.parens() == 20;

Conditional expressions can be nested:

public String ternary(int n) {
return parameters.n > 0 ? "pos" : (parameters.n < 0 ? "neg" : "zero");
}
assert calc.ternary(5) == "pos";
assert calc.ternary(-1) == "neg";
assert calc.ternary(0) == "zero";

&& and || evaluate both operands, as the control flow page shows.

Numbers

Division and remainder give decimal numbers, even between integers. 7 / 2 is 3.5, not 3, 6 / 3 is 2.0, and 7 % 2 is 1.0. The declared type of the behaviour does not change that:

public double intDiv() {
return 7 / 2;
}

public double evenDiv() {
return 6 / 3;
}

public double intMod() {
return 7 % 2;
}
assert calc.intDiv() == 3.5;
assert calc.evenDiv() == 2.0;
assert calc.intMod() == 1.0;

Addition, subtraction and multiplication between integers stay integers, and mixing an integer with a decimal gives a decimal:

public double mixed() {
return 3 * 1.5;
}
assert calc.mixed() == 4.5;

To get an integer from a decimal, use a cast, which drops the decimals:

public int castInt() {
double d = 3.7;
return (int) d;
}
assert calc.castInt() == 3;

A string is never a number. "4.5" * 1.0 is an error. To convert, call a Java method: import the class, and use its simple name.

import java.lang.Double;
public double parsed() {
return Double.parseDouble("4.5") * 2;
}
assert calc.parsed() == 9.0;

Strings

+ concatenates when its left operand is a string, and a number or null on the right is written as text; null is written null:

public String concatNumber() {
return "a" + 1;
}

public String nullConcat() {
String n = null;
return "x" + n;
}
assert calc.concatNumber() == "a1";
assert calc.nullConcat() == "xnull";

The other way round, 1 + "a", is a type error and stops the behaviour.

== compares the contents of two strings, not their identity:

public boolean stringEq() {
String a = "ab";
String b = "a" + "b";
return a == b;
}
assert calc.stringEq();

The Java methods of a string are available on a variable or a parameter. null is tested with == null:

public String upperLocal() {
String s = "abc";
return s.toUpperCase();
}

public String replaceLocal() {
String s = "EQ-12-3";
return s.replace("EQ-", "").replace("-", "");
}

public int lengthOf(String s) {
return parameters.s.length();
}

public boolean isNull() {
String n = null;
return n == null;
}
assert calc.upperLocal() == "ABC";
assert calc.replaceLocal() == "123";
assert calc.lengthOf("hello") == 5;
assert calc.isNull();
caution

A method called directly on a string literal, "hello".length(), gives null, with no message. Put the literal in a variable first, as upperLocal does.

Lists

A Java list is created with new ArrayList<Type>() and used with its Java methods:

public int listSize() {
List<String> l = new ArrayList<String>();
l.add("a");
l.add("b");
return l.size();
}

public String listGet() {
List<String> l = new ArrayList<String>();
l.add("a");
l.add("b");
return l.get(1);
}

public boolean listContains() {
List<String> l = new ArrayList<String>();
l.add("a");
return l.contains("a");
}
assert calc.listSize() == 2;
assert calc.listGet() == "b";
assert calc.listContains();

When a path does not resolve

A path that cannot be resolved is not always an error: in a behaviour it gives null, and the behaviour goes on. Several mistakes end this way, and are covered on the pages where they arise:

The mistakeInstead
a parameter read by its bare name, whoparameters.who
a behaviour of the same instance called bare, twin()this.twin()
a concept of another model used without importing itimport [uri];
a concept that is not nested created with a bare newcontainer.new
super.name() in a concept with several parentsa helper behaviour called on this
a method called on a string literala variable

When a value is null and should not be, look for one of these first, and validate the model, which reports unresolved paths.

What you have seen

Expressions read like Java, with the usual precedence and a conditional operator. Division and remainder give decimal numbers, a string is never converted to a number, + with a string on the left concatenates, and both sides of && and || are evaluated. A path that does not resolve gives null rather than an error, which is the first thing to suspect when a value is missing.

The model and script (Calc.fml and TestCalc.fmlscript) are tests of the platform, in the openflexo-core repository.