Fantom

 

3. Tour

Hello World

We start our whirlwind tour of Fantom's features, with the quintessential hello world:

class HelloWorld
{
  static Void main()
  {
    echo("hello world")
  }
}

Minor differences from Java or C# include:

  • all type names are capitalized including Void (Fantom doesn't have primitives nor primitive keywords).
  • Class and method protection scope default to public.
  • Fantom sports the echo method for writing to the console or you can use Env.cur.out.
  • Statements can be terminated with a newline (you can use a semicolon too)
  • You can declare Str[] args or access them from Env.args

Literals

Fantom supports the same primitive literals as Java or C#, plus some that are typically found in higher level scripting languages:

true                // Bool literal
123                 // Int literal
0xcafe_babe         // Int hex, can use _ as separator
'\n'                // Int character literal
"hi"                // Str literal
3.4f                // Float literal
3.4d                // Decimal literal
5sec                // Duration literal
`/dir/file.txt`     // Uri literal
[0, 1, 2]           // List literal
[,]                 // List literal for empty list
[1:"one", 2:"two"]  // Map literal
[:]                 // Map literal for empty map
Int#                // Type literal
Int#plus            // Slot literal

Expressions

Fantom reuses most the same expression syntax as Java and C#:

obj.toStr()   // call the toStr method
obj.toStr     // parenthesis are optional
obj.field     // access field
x && y        // logical and
x === y       // reference equality
x == y        // shortcut for x.equals(y)
x < y         // shortcut for x.compare(y) < 0
x <=> y       // shortcut for x.compare(y)
x + y         // shortcut for x.plus(y)
x = y         // assignment
a ? b : c     // ternary operator
a is Str      // instance of operator
a isnot Str   // convenience for !(a is Str)
a as Str      // like C# as operator
a?.func()     // safe invoke operator (like Groovy)
a ?: b        // elvis operator for a != null ? a : b

Most operators are actually just syntax sugar for a method call. For example:

3 + 4  =>  3.plus(4)

Strings

Strings support interpolation which allows you to embed expressions via the "$" character:

// string interpolation
"$x + $y = ${x+y}"

// longhand for above
x.toStr + " + " + y.toStr + " = " + (x+y).toStr

A simple variable or dotted expression can just be prefixed with "$", but more complicated expressions are wrapped in curly braces.

Fantom also has built-in support for localized strings:

// lookup a locale props value
"$<fwt::err.name>"

// add locale lookup and automatically add default
// key/value to the pod's locale/en.props resource file
"$<fileNotFound=The file was not found>"

Statements

Statements are mostly like Java and C#:

Str s := "hello"    // local variable declaration
s := "hello"        // type inference
if (x) {} else {}   // if/else statement
while (x) {}        // while

The most noticeable difference is the use of := to declare a local variable. Fantom uses type inference, so you can omit a local's type declaration. The := operator is used instead of = to show the intention of declaration versus assignment. It prevents silly mistakes like variable name typos.

Fields

Fields automatically include support for accessor methods without a lot of verbosity:

class Person
{
  Str name
  Int age
}

The code above is basically equivalent to this Java code:

public class Person
{
  public String name() { return name; }
  public void name(String x) { name = x; }

  public int age() { return age; }
  public void age(int x) { age = x; }

  private String name;
  private int age;
}

The reason Java and C# programmers write such tortured code is on the off-chance that they want to trap when a field is get or set. In Fantom, fields are always accessed via an auto-generated getter or setter which you can override:

class Person
{
  Str name
  Int age { set { checkAge(val); &age = it } }
}

The it keyword denotes the value being used to set the field. The syntax &age denotes that we are setting the actual storage location for the age field (not using the setter).

Methods

Method arguments can have default values:

class Person
{
  Int yearsToRetirement(Int retire := 65) { return retire - age }

  Int age
}

Once methods only compute their result the first time they are called and then return a cached value on subsequent calls:

once Str fullName() { return "$firstName  $lastName" }

Constructors

One thing different about Fantom is that constructors are named methods:

class Point
{
  new make(Int x, Int y) { this.x = x; this.y = y; }
  Int x
  Int y
}

// make a point
pt := Point.make(30, 40)   // longhand
pt := Point(30, 40)        // shorthand

By convention, the primary constructor is called make and other constructors are prefixed with "make". If you don't provide a constructor, the compiler will auto-generate one for you called make.

Inheritance

The syntax for inheritance looks just like C#:

class Animal
{
  virtual Void talk() { echo("talk") }
}

class Dog : Animal
{
  override Void talk() { echo("bark") }
}

Note the use of virtual and override keywords. Unlike Java methods must be explicitly marked virtual - you have to design for a method to be overridden.

Mixins

Java and C# use interfaces to implement multiple type inheritance. Fantom mixins are like interfaces but can declare method implementations:

mixin Audio
{
  abstract Int volume
  Void incrementVolume() { volume += 1 }
  Void decrementVolume() { volume -= 1 }
}

class Television : Audio
{
  override Int volume := 0
}

The best way to explain what the Fantom code does above is to map it to its Java equivalent:

interface Audio
{
  int volume();
  void volume(int volume);
  void incrementVolume();
  void decrementVolume();
}

class AudioImpl
{
  static void incrementVolume(Audio self) { self.volume(self.volume() + 1); }
  static void decrementVolume(Audio self) { self.volume(self.volume() - 1); }
}

class Television implements Audio
{
  int volume() { return volume; }
  void volume(int x) { volume = x; }

  void incrementVolume() { AudioImpl.incrementVolume(this); }
  void decrementVolume() { AudioImpl.incrementVolume(this); }

  private int volume = 0;
}

Notice the use of an abstract field. Like interfaces, a mixin can't actually contain any state. But mixins can declare abstract fields which define getters and setters, but no actual storage.

Closures

Fantom supports first class functions and closures - the standard APIs make heavy use of functional programming. Closure syntax kind of, sort of looks like Ruby. For example iteration over collections is almost always done using closures:

// print a list of strings
list := ["red", "yellow", "orange"]
list.each |Str color| { echo(color) }

// print 0 to 9
10.times |Int i| { echo(i) }

The signature of a closure function can be inferred from its context:

10.times |i| { echo(i) }

It-blocks are a special form of closure with an implied parameter of it:

10.times { echo(it) }

Closures are also used to pass chunks of code into standard methods:

// sort a list of files by timestamp
files = files.sort |a, b| { a.modified <=> b.modified }

Closures are really just an expression that creates a Func instance:

// create a function that adds two integers
add := |Int a, Int b->Int| { return a + b }
nine := add(4, 5)

Dynamic Programming

Fantom provides some key features to break free from the shackles of strong typing when needed. When you access a field or method using the "." dot operator the compiler does type checking. But you can also use the "->" dynamic call operator to skip type checking:

obj->foo         // obj.trap("foo", [,])
obj->foo(2, 3)   // obj.trap("foo", [2, 3])
obj->foo = 7     // obj.trap("foo", [7])

The "->" operator is really just syntax sugar for invoking the Obj.trap method which by default uses reflection to call a method or access a field. But you can also override the trap method to handle dynamic calls in imaginative ways.

Another feature to make dynamic programming nice is the use of Obj as a wildcard. You can assign Obj to anything or pass it to a method call without an explicit cast:

Obj obj
Str s := obj->foo     // Str s := (Str)obj->foo
Int.fromStr(obj)      // Int.fromStr((Str)obj)

Nullable Types

Types may be nullable or non-nullable. A non-nullable type is guaranteed to never store the null value. Nullable types are indicated with a trailing "?". This means non-nullable is the default unless otherwise specified:

Str   // never stores null
Str?  // might store null

The compiler prevents obvious mistakes like using the null literal when a non-nullable type is expected. Additional checks are implicitly done at runtime when coercing a nullable type to a non-nullable type. This allows your code to fail fast at the point where null bug was introduced versus propagating into unrelated code.

Serialization

The serialization syntax of Fantom is a subset of the actual programming language. This means you use serialization to build up arbitrarily complex object structures in code. Plus it makes serialized objects easy for us humans to read and write:

@Serializable class Person
{
  Str? name
  Int age
  Person[]? children
}

// built up tree of objects in code (or from file)
homer := Person
{
  name = "Homer Simpson"
  age  = 39
  children =
  [
    Person { name = "Bart";   age = 7 },
    Person { name = "Lisa";   age = 5 },
    Person { name = "Maggie"; age = 1 }
  ]
}

// dump serialized structure to console
Env.cur.out.writeObj(homer, ["indent":2])

Immutability

Classes may be declared const to create immutable classes:

const class Point
{
  new make(Int x, Int y) { this.x = x; this.y = y }
  const Int x
  const Int y
}

Const classes contain only fields which are themselves const and set in the constructor. To prevent threads from sharing state, static fields must always be const:

const static Point origin := Point(0, 0)

Lists and Maps can be declared immutable using the toImmutable method:

vowels := ['a','e','i','o','u'].toImmutable

Actors

Fantom includes an actor framework for building concurrent applications:

// spawn actor which aynchronously increments an Int msg
actor := Actor(group) |Int msg->Int| { msg + 1 }

// send some messages to the actor and block for result
for (i:=0; i<5; ++i) echo(actor.send(i).get)