Skip to main content

Objects and Nullability

An object is a reference type with named fields. Use objects for application data, parsed messages, configuration, and shared mutable state.

Defining an object​

object Point {
float x;
float y;
}

Objects can define methods. Inside a method, this is the receiving object:

object Player {
string name;
int score;
Point position;

infallible fn move(float dx, float dy) -> void {
this.position.x = this.position.x + dx;
this.position.y = this.position.y + dy;
}
}

Call a method with .: player.move(2.0, -1.0);.

Constructing an object​

An object literal supplies fields by name:

Point origin = Point {
x: 0.0,
y: 0.0
};

Player player = Player {
name: "Ada",
score: 0,
position: origin
};

Fields may appear in any order, each at most once. A trailing comma is allowed.

Omitted number, char and bool fields start at zero or false, and an omitted enum field at its member valued 0. Omitted nullable fields start as null. Every other field, including string and an enum with no member valued 0, must be supplied.

Reading and writing fields​

int old_score = player.score;
player.score = old_score + 10;

Field types are checked statically.

Const views​

const T is a read-only view of an object, interface or engine type:

infallible fn inspect(const Player player) -> int {
return player.score;
}

A Player may flow into const Player. The reverse is rejected for assignments, parameters, returns, branch results, and captured values.

Through a const view, fields are readable but not assignable. Methods you write can't be called through a const view, because they may change the object. Some engine types have methods that can be.

Const is shallow: a reference read from a const view is not itself const.

Private fields​

Prefix a field with ~ to make it private to the package (folder) that declares the object:

object Counter {
~int value;

infallible fn increment() -> void {
this.value = this.value + 1;
}

infallible fn current() -> int {
return this.value;
}
}

infallible fn counter_create() -> Counter {
return Counter { value: 0 };
}

Any file in the same package may read, write, and initialize value. Other packages can neither access it nor name it in an object literal; they use counter_create, increment, and current. This keeps representation private behind constructor functions and methods. Fields without ~ are public.

The same ~ before object makes the whole type private to its package (~object Cursor { ... }); see Private declarations. A public object may hold one only in a ~ field. ~ before a method is an error: methods are public whenever the object is usable.

Interfaces​

An interface names a set of required methods:

interface Counter {
infallible fn increment(int amount) -> void;
infallible fn current() -> int;
}

An object satisfies an interface when its methods match. There is no implements declaration:

object Score {
int value;

infallible fn increment(int amount) -> void {
this.value = this.value + amount;
}

infallible fn current() -> int {
return this.value;
}
}

Score score = Score { value: 10 };
Counter counter = score;
counter.increment(5);

Interfaces are reference types. They may be parameters, results, locals, fields, nullable references, collection elements, and task results. Converting an object to an interface aliases the same object; it does not copy or wrap it.

Conformance is checked at compile time. Parameter and result types must match exactly. An infallible method satisfies a fallible requirement, but a fallible method does not satisfy an infallible one.

To find out what an interface value really holds, test it with is, and get the object back with a downcast:

if (counter is Score) {
Score original = Score(counter)?;
original.value = 0;
}

value is T is false for null. T(value) fails with code 3 when the value is null or not a T, so it needs ? or !!. T may also be another interface: Named(counter)? succeeds when the object has Named's methods.

examples/interfaces/interfaces.bt is a complete program in which two object types share one interface.

References and aliases​

Objects are references. Assignment creates another alias to the same object; it does not copy fields.

Player first = player;
Player second = first;
second.score = 50;
// first.score and player.score are now also 50.

To get independent state, create a new object and copy the fields you need. Arrays, maps, channels, tasks, errors, buffers, interfaces, and engine objects alias the same way.

Equality​

== on objects compares identity: two references are equal when they refer to the same object. Fields are not compared. Strings are values, not references, and == compares their contents.

Nullable references​

Reference types are non-null by default. Add ? to permit null:

Player? selected = null;

if (selected == null) {
println("Nothing selected");
}

Scalar types cannot be nullable: numbers, bool, char, enums, string, and function types.

A T? is not assignable to T, and a null check does not narrow it. Reading a field through null traps at runtime; the compiler does not reject it. The same holds for built-in methods on a nullable collection: items.length() or queue.send(v) on an array<T>? or channel<T>? compiles without a check and traps if the value is null (a null array's length() traps rather than returning 0). Check before access:

if (selected != null) {
println(selected.name + ": " + int_to_string(selected.score));
}

Prefer non-null values in the core of a function.

Engine objects​

cTurtle provides object types of its own. You use their fields like any object's. For example, Weapon has ammo and magazine_size:

include "cturtle/world";

infallible fn reload(Weapon weapon) -> void {
if (weapon.ammo == 0) {
weapon.ammo = weapon.magazine_size;
}
}

The cTurtle API reference lists these types, their fields and functions, and which operations can fail. Each lives in a package your code must include.

Some engine objects stand for data that can disappear, such as an entity's component. Reading or writing their fields can fail, so each access takes ?: (velocity.x = 2.0)?. If the entity has been destroyed, the access fails with error code -410, which you can handle with !! like any other error. The entities, components and systems guide shows this in use.