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:
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:
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!