const classes are mentioned a lot in Fantom, especially in the topics of IoC and services. const classes are defined by the const keyword and look like this:

const class MyClass {
    const Int? value
}

But what makes it different to a standard non-const class?

class MyClass {
    Int? value
}

I shall explain.

In a nut shell, const classes are immutable. That is:

Once created, the data they hold can never change!

Now, just like any other class, a const class holds its data in fields. But, to make sure everyone knows the field values can never change, they too, must be defined with the const keyword. And now, that value is constant. In fact, it's a compilation error to even attempt to change it!

const class MyClass {
    const Int? value
    Void main() {
        value = 69 // --> Err: Cannot set const field 'value' outside of constructor
    }
}

See! So how do you set values on const fields?

Setting Const Values

Values of const fields can only be set on class initialisation. That means either inline during field declaration, or in the ctor.

const class MyClass {
    const Str setViaField  := "wotever"
    const Str setViaCtor

    new make() {
        setViaCtor = "wotever"
    }
}

Note, non-const classes can still contain const fields. These const fields still need to be set during initialisation.

Fantom const vs Java final

Fantom const fields are similar to Java final fields, only better! Java final fields always seemed broken to me, for consider:

import java.util.ArrayList;
import java.util.List;

public class JavaClass {
    final List<String> finalList = new ArrayList<String>();
}

In Java, the list reference is not allowed to change, but the contents can!? That means I can add to it like this:

select all
import java.util.ArrayList;
import java.util.List;

public class JavaClass {
    final List<String> finalList = new ArrayList<String>();

    public void add() {
        finalList.add("wotever");
    }
}

And it works! Bonkers! That's not very final to me!

The difference in Fantom is that nothing is allowed to change, not lists, not maps, not references, nothing! Which brings me to the next point.

Const classes can only contain immutable data.

Sounds obvious, until you realise that they can't hold a Buf, a StrBuf, or an xml::XElem, or a.... Any object that is not const can not be a field in a const class.

const class MyClass {
    const StrBuf? notConst  // --> Err: Const field 'notConst' has non-const type 'sys::StrBuf?'
}

const classes are truly immutable! In fact, some may say they're useless and boring; for once created, that's it - job done! They can't alter their state in anyway! (*)

And that's why they're thread safe. A const class can be passed (by reference) to anyone, in any thread, and there are never any synchronisation issues or race conditions.

(*) Okay, I'm sorta telling a fib here - const classes do have access to mutable state. See From One Thread to Another... for more details.

Maps and Lists

But what of Maps and Lists? These may not be const but they can still be used in const classes because they're special. Maps and Lists have the concept of being immutable. As soon as you assign them to a const field, their contents are locked down and can never change again. No other Fantom class can do this, not even your own classes, that's why they're special.

const class MyClass {
    const Str[] constList  := ["wot", "ever"]
}

Nice, now what if you want to generate list data on the fly and set it in the ctor? Well, there's a right way and a wrong way. This is the wrong way to add data to a const List:

const class MyClass {
    const Str[] constList

    new make() {
      constList = [,]          // constList is *locked down*
        constList.add("data")  // --> Err: sys::ReadonlyErr: List is readonly
    }
}

Once you've set the constList, you can not add data to it. To get around this, make a local list first:

select all
const class MyClass {
    const Str[] constList

    new make() {
      localList := [,]
        localList.add("data")
        ...
        constList = localList
    }
}

Advanced - The it-block ctor

Thanks to the it-block ctor there's another way to set const fields. Take this class for example:

const class MyClass {
    const Str value1
    const Str value2

    new make(|This| f) {
        f(this)
    }
}

The first thing to notice is No Compilation Err! The const fields are not set in the ctor, yet Fantom doesn't complain. That's because Fantom is anticipating what's coming next!

We can create an instance of MyClass by passing in a block, which takes MyClass (or This) as an argument. The f(this) executes the block, passing in itself as the argument. Fantom is expecting this block to set the const fields.

myClass := MyClass() {
    it.value1 = "wot"
    it.value2 = "ever"
}

If you forgot to set a const field then you get a RuntimeErr:

myClass := MyClass() {
    it.value1 = "wot"
}    // --> Err: sys::FieldNotSetErr: myPod::MyClass.value2

You can use this technique to override any const field value. It is used a lot in the core Fantom API. For example, ActorPool is defined as:

const class ActorPool {
    new make(|This|? f := null) {...}

    const Int maxThreads := 100
}

But you can change the default value of maxThreads like this:

actorPool := ActorPool() {
    it.maxThread = 3
}

Mega Advanced - Reflection via it-block ctor

On a final note, if you want to set const fields via reflection, you may be tempted to do this:

actorPool := ActorPool() {
    ActorPool#maxThreads.set(it, 3)
}

But you'll only get:

sys::ReadonlyErr: Cannot set const field concurrent::ActorPool.maxThreads

Instead you have to use a special function that Field creates for you:

f := Field.makeSetFunc([ActorPool#maxThreads : 3])
actorPool := ActorPool(f)

Have fun!


Discuss