13. 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 const class
  • 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.

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.