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 useEnv.cur.out
. - Statements can be terminated with a newline (you can use a semicolon too)
- You can declare
Str[]
args or access them fromEnv.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)