Methods
Overview
A method is a slot which defines a function within a class or mixin:
class Boo { static Int add(Int a, Int b) { return a + b } Int incr() { return count++ } Int count := 0 }
In the example above add
and incr
are method slots on the class Boo
. The incr
method is an instance method which means it is always invoked on an instance of Boo
. The add
method is static and is not invoked on an instance:
b := Boo() x := b.incr() y := Boo.add(3, 4)
Method invocation is performed using the .
dot operator on a target. The target for instance methods is an instance of the type; for static methods the target is the type name.
Methods in your own type (or types you inherit) are automatically scoped such that the target type or instance is implied. For example:
class Foo : Boo { Int more() { return incr() + add(3, 4) } }
If the method does not take any parameters, then we can leave off the ()
empty parentheses. By convention the empty parentheses are always omitted:
b.incr // same as b.incr()
You can also the ?.
operator to safely handle a null target. See safe invokes.
This
Instance methods always have an implied first parameter which is the instance itself identified via the keyword this
. The definitions of a
and b
are identical in the following example:
class Foo : Boo { Int a() { return incr } Int b() { return this.incr } }
Constructors
Constructors are special methods used to create new instances of a class. In Fantom, constructors are named methods. The difference is that they use the new
keyword in their definition instead of a return type (the return type is implied to be an instance of the type).
class MissingPerson { new make(Str name) { this.name = name } Str name }
By convention, the primary constructor should be called make
and other constructors should be prefixed with make
. Like other slots, constructors must be uniquely named within their type. To create an instance, you call the constructor like a static method:
jack := MissingPerson.make("Jack Shephard")
You can also use the shorthand syntax:
sayid := MissingPerson("Sayid Jarrah")
Fantom supports both instance constructors and static constructors. From a client perspective, both instance and static constructors look just like named factory methods. Instance constructors have an implicit object allocation, so the body of the method works just like any other instance method with an implied this
instance. Static constructors on the other hand are normal static methods and it is your responsiblity to perform an object allocation (using an instance constructor).
class Number { new make(Int val) { this.val = val } static new fromStr(Str s) { make(s.toInt) } }
In the example above we have an instance constructor named make
and a static constructor named fromStr
. Notice that the instance constructor has an implicit this
parameter - the allocation is performed automatically when called. However since fromStr
is a static constructor, it is responsible for performing the allocation (in this case delegating to make
). When it comes calling constructors they both work the same way:
x1 := Number.make(1) x2 := Number(2) x3 := Number.fromStr("3") x4 := Number("4")
The return type of a static constructor is always a nullable version of the defining class. So in the example above Number.fromStr
has an implied return type of Number?
. The return type of an instance constructor is Void
, but when called by a client will evaluate to the defining type.
Only classes can have instance constructors. It is a compile time error to declare an instance constructor on a mixin. However, mixins are allowed to declare static constructors.
Auto Generated Constructor
If you do not declare any instance constructors on your class, then the compiler will automatically generate a public no arg constructor called make
.
Construction Calls
Fantom supports a special syntax called construction calls with the syntax Type(args)
. Like operators, these calls support overloading by parameter type. Any constructor method marked with the new
keyword may be used with a constructor call.
Convention is to always prefer a construction call to using make
explicitly:
ArgErr.make // non-preferred ArgErr() // preferred ArgErr.make("bad arg") // non-preferred ArgErr("bad arg") // preferred
If the compiler cannot determine which constructor is being called from the arguments it will report a "Ambiguous constructor" error. In this case you will need to explictly use your constructor name.
Constructor Chaining
When creating subclasses, you must call one of your parent class instance constructors or another of your own constructors using a syntax called constructor chaining. The syntax to call a parent constructor is based on C++ and C# using the :
after the formal parameters, but before the method body:
class Foo { new make() {} new makeName(Str name) {} } class Bar : Foo { new make() : super() {} new makeFullName(Str? first, Str last) : super.makeName(last) {} new makeLastName(Str last) : this.makeFullName(null, last) {} }
All constructor chains start with the this
or super
keyword. Use this
to chain to one of your own constructors or super
to call a parent constructor. Then the constructor to call is specified as a normal method call with the name and argument list. As a shortcut, you can omit the name if the parent constructor being called has the same name.
In the example above, Bar.make
illustrates calling Foo.make
- omitting the name implies calling a parent of the same name - make
in this case. Bar.makeFullName
illustrates calling a super class constructor by name. Bar.makeLastName
shows how to call a peer constructor on your own class - this is useful for ensuring all your initialization code is centralized in one constructor.
Static Initializers
Static initializers are specially methods executed during class initialization. They are typically used to initialize static fields. They use a Java like syntax:
class Foo { static { echo("initializing Foo...") } }
Assignment to static fields is done in an auto-generated static initializer. It is permissible to have multiple static initializers, in which case they are run in the order of declaration:
class Foo { static const Int a := 10 static { echo("1st a=$a b=$b") } static const Int b := 20 static { echo("2nd a=$a b=$b") } static { a = 30 } static { echo("3rd a=$a b=$b") } } // outputs 1st a=10 b=null 2nd a=10 b=20 3rd a=30 b=20
Default Parameters
You can specify a default argument for parameters. Defaults can be applied to the last zero or more parameters (right to left). For example:
static Int add(Int a, Int b, Int c := 0, Int d := 0) { return a + b + c + d }
In this example the last two parameters c
and d
default to zero. This allows you to call the add
method with 2, 3, or 4 arguments:
add(3, 4, 5, 6) add(3, 4, 5) // same as add(3, 4, 5, 0) add(3, 4) // same as add(3, 4, 0, 0)
Operators
Fantom supports operator overloading using operator methods. Operator methods are just normal methods which are annotated with the @Operator
marker facet. The following naming conventions are enforced for determining which operator is used by the method:
prefix symbol degree ------ ------ ------ negate -a unary increment ++a unary decrement --a unary plus a + b binary minus a - b binary mult a * b binary div a / b binary mod a % b binary get a[b] binary set a[b] = c ternary add a { b, }
In the case of the unary and ternary operators the method name must match exactly. For the binary operators, the method must only start with the given name. This allows binary operators to be overloaded by parameter type:
class Foo { @Operator Int plusInt(Int x) { ... } @Operator Float plusFloat(Float x) { ... } } Foo + Int => calls Foo.plusInt and yields Int Foo + Float => calls Foo.plusFloat and yields Float
The compiler performs method resolution of operators using a very simple algorithm. If there are multiple potential matches the compiler will report an error indicating the operator resolves ambiguously. The compiler does not take class hierarchy into account to attempt to find the "best" match.
Virtual Methods
Virtual methods are designed to be overridden by a subclass to enable polymorphism. Methods must be marked using the virtual
keyword before they can be overridden by subclasses. Subclasses must declare they are overriding a method using the override
keyword:
class Animal { virtual Void talk() { echo("generic") } } class Cat : Animal { override Void talk() { echo("meow") } } Animal().talk // prints generic Cat().talk // prints meow
By default when a subclass overrides a method, it is implied to be virtual - its own subclasses can override it again. You can use final
keyword to prevent further overrides:
class Lion : Cat { override final Void talk() { echo("roar!") } }
Abstract Methods
Abstract methods are virtual methods without an implementation. They are declared using the abstract
keyword. Abstract methods are implied to be virtual - it is an error to use both the abstract
and virtual
keyword. Abstract methods must not provide a method body. If declared within a class, then the containing class must also be abstract
.
Once Methods
The once
keyword can be used to declare once methods. A once method only computes its result the first time it is called and then returns a cached value on subsequent calls. Once methods are a great technique for lazily creating state without a lot of boiler plate code:
// hard way Str fullName { get { if (&fullName == null) &fullName = "$firstName $lastName" return &fullName } } // easy way once Str fullName() { return "$firstName $lastName" }
Restrictions for once methods:
- Must not be declared within a mixin
- Must not be a constructor
- Must not be static
- Must not be abstract
- Must return non-Void
- Must have no parameters
If a once method throws an exception, then there is no cached value - subsequent calls will re-execute the method until it returns a value.
A once method may be used on a const class with caveats. In the JVM the cache field is compiled to a volatile field. However, there is no guarantee that the method is called exactly once across multiple threads. So the computation must be a pure function that always returns the same value. Furthermore there is no guarantee that all threads see the exact same instance returned.
Covariance
Fantom supports covariance - which allows an overridden method to narrow the return type of the inherited method:
abstract class Animal { abstract Animal mommy() abstract Animal daddy() } class Cat : Animal { override Cat mommy() {...} override Cat daddy() {...} }
This Returns
A method declared to return This
is a special case of covariance which always returns the type being used. This technique is typically used by methods which return this
to enable method chaining. Consider this example:
class Connection { Connection open() { return this } } class MyConnection : Connection { MyConnection talk() { return this } }
The APIs are written to allow method chaining, so we'd like to be able to write something like this:
MyConnection.make.open.talk
If you actually tried to compile that code you'd get an error like "Unknown slot Connection.talk". We could write code without method chaining, or we could even use the "->" operator. But this technique is so commonly used, that Fantom allows you to declare the return type as This
:
class Connection { This open() { return this } } class MyConnection : Connection { This talk() { return this } }
The This
type is a special marker type like Void
. It indicates that a method is guaranteed to always return an instance of the target type. In our example above, the expression x.open
will always evaluate to an instance of Type.of(x)
.
Use of This
is restricted to the return type of non-static methods. You can't use it for static methods, parameter types, local variable types, or for fields. Overrides of a methods which return This
must also return This
.
Dynamic Invoke
As any dynamic language proponent can tell you - sometimes static typing can be a real pain. So Fantom supports a hybrid static/dynamic design by providing two call operators. The .
dot operator accesses a slot using static typing - if the slot cannot be resolved at compile time, then it results in a compile time error.
The ->
dynamic invoke operator lets you perform calls with no compile time type checking. What dynamic invoke actually does it generate a call to the Obj.trap
method. By default the trap
method uses reflection to lookup and call the method. If the name maps to a field, then trap
will get or set the field depending on the number of arguments:
a->x a.trap("x", [,]) a->x = b a.trap("x", [b]) a->x(b) a.trap("x", [b]) a->x(b, c) a.trap("x", [b, c])
In the simplest case, the ->
operator is syntax sugar to by-pass static type checking and use reflection. But the ability to override the trap
method is a powerful technique in the Fantom toolkit for building dynamic solutions.
You can also the ?->
operator to safely handle a null target. See safe invokes.
Native Methods
Native methods are implemented in an alternate language which is "native" for each target platform. Native methods are typically written in Java for the Java VM and C# for the .NET CLR. Native methods use the native
keyword and must not have a method body (like abstract methods). The infrastructure for supporting native methods is discussed in the Natives chapter.