Fantom

 

3. Expressions

Operator Precedence

Fantom's expression syntax is very similar to C, Java, C# and company. Operators in order of precedence:

  • Primary: (x) x.y x.y() x->y() x?.y x?.y() x?->y() x[y]
  • Unary: ++x --x x++ x-- !x +x -x (T)x &x
  • Multiplicative: * / %
  • Additive: + -
  • Range: .. ..<
  • Relational: < <= >= > <=> is isnot as
  • Equality: == != === !==
  • Conditional And: &&
  • Conditional Or: ||
  • If Expr: x?t:f x?:y
  • Assignment: = *= /= %= += -=
  • Collection Add: ,

Shortcut Operators

Fantom is a pure OO language in that everything is an object you can call methods on - even value-types such as Bool and Int. As such almost all the operators are really just method calls. We call these operators the shortcut operators because they are just syntax sugar for calling a specific method:

a + b     =>  a.plus(b)
a - b     =>  a.minus(b)
a * b     =>  a.mult(b)
a / b     =>  a.div(b)
a % b     =>  a.mod(b)
a[b]      =>  a.get(b)
a[b] = c  =>  a.set(b, c)
-a        =>  a.negate()
++a, a++  =>  a = a.increment()
--a, a--  =>  a = a.decrement()
a == b    =>  a.equals(b)
a != b    =>  !a.equals(b)
a <=> b   =>  a.compare(b)
a > b     =>  a.compare(b) > 0
a >= b    =>  a.compare(b) >= 0
a < b     =>  a.compare(b) < 0
a <= b    =>  a.compare(b) <= 0
a,b,c     =>  it.add(a).add(b).add(c)

For example say we have two variables a and b both of type Int. Then the expression a+b is really just syntax sugar for calling Int.plus as a.plus(b). See Method Operators for a detailed discussion.

Prefix and Postfix Operators

The ++ and -- operators can be prefix or postfix just like C family languages. Both of these operators assign the result of the call to increment or decrement to the operand variable. If prefix then the expression evaluates to the assignment. If postfix then the expression evaluates to the value of the operand before increment or decrement is assigned.

Equality Operators

The equality operators == and != both make use of the Obj.equals virtual method. Most types override this method to compare value equality. If equals is not overridden, then the default behavior is to compare reference equality.

Relational Operators

The relational operators like < and > all use the Obj.compare virtual method. Many types with the notation of ordering will override this method to return -1, 0, or 1. If compare is not overridden, then the default implementation will compare the result of the operands toStr method.

The compiler translates the numeric return into a boolean condition based on which operator was used. The special <=> operator returns the Int value of -1, 0, 1 directly. You will commonly use the <=> operator for custom sorts with a closure:

people.sort |Person a, Person b->Int| { return a.age <=> b.age }

If that code doesn't make any sense to you, then don't worry - just keep reading until we cover closures.

Comparisons with Null

The equality and relational operators have special handling if either operand is null such that a NullErr exception is never raised. For equality a non-null and null are never equal, but two nulls are always equal. For relational operators, null is always less than a non-null object. Special handling for null does not apply if the equals or compare method is used as a normal method call. Nor does this special handling apply for other shortcut operators.

Same Operators

The === and !== operators are called the same and not same operators. These operators are used to check if two variables reference the same object instance in memory. Unlike the == and != shortcut operators, the same and not same operators do not result in the equals method call. These operators are not allowed to be used against value-types.

Conditional Operators

The conditional !, &&, and || operators are used with boolean expressions. Use && to perform a logical and and || to perform a logical or. Both of these operators are short circuiting in that the second test is skipped if the first test is conclusive ('false' for && and true for ||). The ! operator performs a logical not. Code examples for the conditional operators:

t := true
f := false
t && f  => evaluates to false
t && t  => evaluates to true
f || t  => evaluates to true
!t      => evaluates to false

Ternary Operator

The ternary operator combines three expressions as a convenient way to assign a value based on an if/else condition:

condExpr ? trueExpr : falseExpr

The condExpr must evaluate to a boolean. If condExpr evaluates to true then the whole expression evaluates to trueExpr, otherwise to falseExpr. Examples:

3 > 4 ? "yes" : "no"  => evaluates to "no"
6 > 4 ? "yes" : "no"  => evaluates to "yes"

Fantom also supports use of a throw statement as one of the results of a ternary operation:

val := isValid(key) ? map[key] : throw ArgErr("invalid key")

Null Convenience Operators

Fantom supports several of the operators found in Groovy to make working with null more convenient:

  • Elvis Operator x ?: y (look at it sideways as a "smiley" face)
  • Safe Invoke x?.y
  • Safe Dynamic Invoke x?->y

Elvis Operator

The elvis operator evaluates the left hand side. If it is non-null then it is result of the whole expression. If it is null, then the result of the whole expression is the right hand side expression. The right hand side expression is short circuited if the left hand side evaluates to non-null. It is similar to how you might use the ternary operator:

// hard way
file != null ? file : defaultFile

// easy way
file ?: defaultFile

The elvis operator may not be used on a non-nullable type since by definition it will not be null.

Like the ternary operator the elvis operator may use a throw statement as the right hand side of the expression:

val := map[key] ?: throw ArgErr("key not found")

Safe Invoke

The safe invoke operators are designed to short circuit if the target of method call or field access is null. If short circuited, then the whole expression evaluates to null. It is quite useful to skip checking a bunch of values for null during a call chain:

// hard way
Str? email := null
if (userList != null)
{
  user := userList.findUser("bob")
  if (user != null) email = user.email
}

// easy way
email := userList?.findUser("bob")?.email

If at any point in a null-safe call chain we detect null, then the whole expression is short circuited and the expression evaluates to null. You can use ?-> as a null-safe version of the dynamic invoke operator.

The safe invoke operator may not be used on a non-nullable type since by definition it will not be null. The result of a safe invoke is always nullable:

x := str.size   =>  x is typed as Int
x := str?.size  =>  x is typed as Int?

Type Checking

The cast operator is used to perform a type conversion. The cast syntax uses parenthesis like C languages - such as (Int)x. If a type cast fails at runtime, then a CastErr exception is raised.

The is, isnot, and as operators are used check an object's type at runtime:

  • is operator returns a Bool if the operand implements the specified type (like Java's instanceof operator). If target is null, then evaluates to false.
  • isnot operator is semantically equivalent to !(x is Type). If target is null then evaluates to true.
  • The as operator returns the object cast to the specified type or null it not an instance of that type (like C#):
Obj obj := 123
obj is Str     =>  evaluates to false
obj is Num     =>  evaluates to true
obj isnot Str  =>  evaluates to true
obj isnot Num  =>  evaluates to false
obj as Float   =>  evaluates to null
obj as Int     =>  evaluates to 123 (expr is typed as Int)

Nullability of types is not considered when using the is, isnot, and as operators. For example these two expressions are considered equivalent:

obj is Str
obj is Str?

The as operator by definition returns a nullable type. For example the following expression evaluates to Str?, not Str:

x := obj as Str  => x is typed as Str?

Indexing

Depending on how it is used, the [] operator maps to three different shortcuts:

a[b]      =>  a.get(b)
a[b] = c  =>  a.set(b, c)
a[b]      =>  a.getRange(b) if b is Range

Typically a[b] is a shortcut for calling a.get(b). For example the List.get method allows you to lookup a list item by it's Int index. Whenever a class supports a get operator method which is annotated with the @Operator facet you can use [] as a shortcut. Consider this code:

list := ["a", "b", "c"]
list.get(2)
list[2]
list.get("2")  // error
list["2"]      // error

The expression list[2] is exactly the same code as list.get(2). The last two lines result in a compiler error because we are attempting to pass a Str when an Int is expected.

When the indexing shortcut is used on the left hand side of an assignment such as a[b] = c then the index operator maps to a.set(b, c). For example these two lines of code have identical behavior:

map.set("tmj", "Too Much Joy")
map["tmj"] = "Too Much Joy"

If the [] operator is used with a Range index, then we map to the a.getRange(b) method which performs a slice. Slicing is used to create sub-strings and sub-lists. Some example code which creates sub-strings:

s := "abcd"
s[0..2]  => "abc"
s[3..3]  => "d"
s[0..<2] => "ab"

start := 0; end := 2
s[start..<end] => "ab"

We use .. to specify an inclusive end index, and ..< to specify an exclusive end index. Also note how we can use any arbitrary expression with the range operators to define compact slice expressions.

By convention Fantom APIs which support integer indexing allow the use of negative integers to index from the end of the list. For example -1 can be used to index the last item of a list (or the last character of a string). Using negative indexes works with all three shortcuts:

list := ["a", "b", "c", "d"]
list[-2]           =>  evaluates to "c"
list[-1] = "last"  =>  replaces list[3] with "last"
list[1..-1]        =>  evaluates to ["b", "c", "last"]

Use of negative indexes applies to most methods on List and Str which take an index argument.

Bitwise Operators

Fantom doesn't have bitwise operators, instead normal method calls are used:

~a      =>   a.not
a & b   =>   a.and(b)
a | b   =>   a.or(b)
a ^ b   =>   a.xor(b)
a << b  =>   a.shiftl(b)
a >> b  =>   a.shiftr(b)

Serialization Expressions

Fantom supports three expression constructs which are designed to make the programming language a true superset of the serialization syntax:

  • Simples
  • It-Blocks
  • Collections

Simples

Simples are special serializable types which serialize via a string represenation. Fantom allows the use of a simple expression:

<type>(<str>)

// for example:
Version("3.2")

// is syntax sugar for
Version.fromStr("3.2")

To use this expression, the type must have a constructor called fromStr which takes a Str parameter and returns an instance of itself. The method may contain additional parameters if they have default values. The type does not have to implement the Serializable facet to use this expression (although it does if you want to serialize it). Simple expressions are a subset of construction calls.

It-Blocks

It-blocks enable you write compound expressions - they are typically used to initialize an instance. This feature is a clean a superset of how complex types are serialized. An example it-block expression:

Address
{
  street = "123 Happy Lane"
  city   = "Houston"
  state  = "TX"
}

// is syntax sugar for (note: can't actually use it as param name)
Address.make.with |Address it|
{
  it.street = "123 Happy Lane"
  it.city   = "Houston"
  it.state  = "TX"
}

Collections

It-blocks may also be used to initialize a collection if it supports a method called "add". Any expression inside an it-block suffixed with a comma is assumed to be a call to it.add:

a,       =>  it.add(a)
a, b     =>  it.add(a).add(b)
a, b, c  =>  it.add(a).add(b).add(c)

Note the comma operator chains the calls to add, therefore the add method must be annotated with the @Operator facet and return the chainable type This.

Here is an FWT example:

Menu
{
  text = "File"
  MenuItem { text = "Open"; onAction.add { echo("open") } },
  MenuItem { text = "Save"; onAction.add { echo("save") } },
}

// is syntax sugar for (note: can't actually use it as param name)
Menu.make.with |Menu it|
{
  it.text = "File"
  it.add(MenuItem MenuItem { text = "Open"; onAction.add { echo("open") } })
    .add(MenuItem { text = "Save"; onAction.add { echo("save") } })
}

Advanced Operators

Fantom has a couple other operators which will be discussed later:

  • Closures are expressions which create a new function inside a method body.
  • Call operator () is used to invoke a function variable.
  • Dynamic invoke operator -> is used to call a method without compile time type checking.
  • Field storage operator & is used to access a field's raw storage without going through its getter/setter methods.