The Loup Scripting Book — contents
  1. Chapter 1: Getting Started
  2. Chapter 2: Variables and Data Types
  3. Chapter 3: Functions and Closures
  4. Chapter 4: Control Flow
  5. Chapter 5: Collections
  6. Chapter 6: Structs
  7. Chapter 7: Enums and Pattern Matching
  8. Chapter 8: Classes and Objects
  9. Chapter 9: Interfaces and Generics
  10. Chapter 10: Error Handling
  11. Chapter 11: Modules and Packages
  12. Chapter 12: Strings, Interpolation, and Template Fences
  13. Chapter 13: Concurrency
  14. Chapter 14: Reflection and Dynamic Compilation
  15. Chapter 15: Reactive UI
  16. Chapter 16: Testing
  17. Chapter 17: JavaScript Host Interop
  18. Chapter 18: Building Script Tools
  19. Chapter 19: Memory Safety and Resource Management

Chapter 8: Classes and Objects

What you'll learn: how to declare classes with fields, a constructor and methods; why methods that change an object are marked modifier and called inside safe; constructors, static factories and visibility; inheritance with super and override; abstract classes; equality; dynamic class; and how objects release resources.

Structs (Chapter 6) bundle data and are copied when you assign them. A class bundles data and behaviour, and its instances are objects that are shared by reference: assigning an object to another variable, or passing it to a function, gives a second name for the same object. Use a class when something has an identity — an account, a session, a parser in the middle of its input — or when you want inheritance.

Defining a Class

@mode(host.scripted)

class Dog {
    pub let name: string;
    pub let breed: string;
    pub var age: int;

    new(name: string, breed: string, age: int) {
        this.name = name;
        this.breed = breed;
        this.age = age;
    }

    pub func bark(): string {
        return i"{$ this.name $} the {$ this.breed $} says: Woof!";
    }

    pub modifier func haveBirthday(): void {
        this.age = this.age + 1;
    }
}

let dog = Dog("Rex", "Labrador", 3);
println(dog.bark());
safe {
    dog.haveBirthday();
}
println(i"{$ dog.name $} is now {$ dog.age $}");
println(dog);

Listing 8-1: A class with fields, a constructor and two methods

Rex the Labrador says: Woof!
Rex is now 4
Dog(name: "Rex", breed: "Labrador", age: 4)

The pieces:

  • Fields are declared with let (cannot change after construction) or var (can change), followed by a type. pub makes a field readable outside the class.
  • new(…) { … } is the constructor. A class has at most one. You create an object by calling the class like a function: Dog("Rex", "Labrador", 3).
  • Methods are functions declared inside the class. Inside a method or the constructor, this is the object the method was called on, and fields are always reached through it: this.name.
  • modifier marks a method that assigns a var field. Calling a modifier method on an object needs a safe { … } block (or a safe func caller); the section "Methods That Change an Object" explains why.
  • println of an object prints its class name and fields.

A class with no : Base clause extends Any, the root of every class (§1-8.3). You will see class Dog : Any in older code; it means the same thing.

Mistake: a field without let or var

If you come from TypeScript or Swift, you may write fields the way structs declare them:

This code does not compile
@mode(host.scripted)

class Point {
    x: int;
    y: int;
}
main.lpscr:4:5: error[E0001]: E_BARE_FIELD: class field `x` must be declared `let` or `var`; write `var x: ...` (mutable) or `let x: ...` (immutable)
main.lpscr:5:5: error[E0001]: E_BARE_FIELD: class field `y` must be declared `let` or `var`; write `var y: ...` (mutable) or `let y: ...` (immutable)

Struct fields need no keyword; class fields always do (ruling FIELD-1). Choose let for a field that is fixed once the object exists, var for one that changes, and add pub if code outside the class reads it: pub let x: int;.

Mistake: leaving out this.

In Java or C#, a method can name a field directly. Loup 1.25 requires this.:

This code does not compile
@mode(host.scripted)

class Dog {
    pub let name: string;
    new(name: string) { this.name = name; }

    pub func bark(): string {
        return i"{$ name $} says: Woof!";
    }
}

println(Dog("Rex").bark());
main.lpscr:8:21: error[E0001]: undefined identifier `name`

The compiler looked for a local variable or parameter called name and found none. Write this.name. The same goes for calling another method of the same object: this.bark(), not bark().

Some examples in the Language Reference read fields and call methods without this. (for instance in §1-8.3 and §1-11.1). The 1.25 compilers do not accept that; always write this..

Mistake: declaring the receiver as a parameter

Python methods take self as their first parameter. In Loup the receiver is implicit, and this is a reserved word:

This code does not compile
@mode(host.scripted)

class Dog {
    pub let name: string;
    new(name: string) { this.name = name; }

    pub func greet(this: Dog): string {
        return i"{$ this.name $} says hi";
    }
}
main.lpscr:7:20: error[E0001]: `this` is a reserved word (§1-3.4) and cannot name a parameter
  note: rename the parameter — e.g. `this_value`

Delete the parameter: pub func greet(): string { … }. this is available in every method without declaring it.

Objects Are Shared by Reference

A variable of a class type holds a reference to an object. Copying the variable copies the reference:

  Struct (value):                    Class (reference):

  let first = Reading(…)             let savings = Account("Ada")
  +---------------------+            +---------+     +----------------+
  | first  celsius=21.5 |            | savings |---->| owner: "Ada"   |
  +---------------------+            +---------+     | balance: 10100 |
                                                     +----------------+
  var second = first     (copy)      let sameAccount = savings   (same object)
  +---------------------+            +-------------+        ^
  | second celsius=21.5 |            | sameAccount |--------+
  +---------------------+            +-------------+

A struct copy is independent of the original:

struct Reading {
    sensor: string;
    var celsius: float;
}

let first = Reading(sensor = "attic", celsius = 21.5);
var second = first;
second.celsius = 30.0;
println(first.celsius);
println(second.celsius);
21.5
30

An object passed to a function is the caller's object, so a change made by the function is visible to the caller:

class Account {
    pub let owner: string;
    pub var balance: int = 0;

    new(owner: string) {
        this.owner = owner;
    }

    pub modifier func deposit(cents: int): void {
        this.balance = this.balance + cents;
    }
}

safe func payInterest(edits acct: Account): void {
    acct.deposit(acct.balance // 100);
}

let savings = Account("Ada");
safe {
    savings.deposit(10000);
    payInterest(edits: savings);
}
println(savings.balance);

let sameAccount = savings;
println(sameAccount == savings);
println(Account("Ada") == Account("Ada"));

Listing 8-2: A function changes the caller's object; == compares identity

10100
true
false

By default == on objects asks "is this the same object?", so two separately created accounts with the same contents are not equal. "Equality" below shows how to define your own ==.

Methods That Change an Object: modifier and safe

Because many references can point at one object, Loup makes mutation of an object visible in the source code at three places (§3-2.4.11, §3-2.4.12):

  1. A method that assigns a var field is declared modifier.
  2. A call to a modifier method — or a direct assignment to a pub var field from outside the class — happens inside a safe { … } block or inside a safe func.
  3. A function that changes an object it receives declares the parameter edits, and the caller writes edits: at the call site.

Listing 8-2 shows all three. The rules let a reader find every place an object can change, and they let the compiler check — inside safe — that no other reference is being used while the change happens.

Mistake: forgetting modifier

This code does not compile
@mode(host.scripted)

class Counter {
    pub var count: int = 0;

    pub func bump(): void {
        this.count = this.count + 1;
    }
}
main.lpscr:7:9: error[E_MODIFIER_REQUIRED]: E_MODIFIER_REQUIRED: writing to `var` field 'count' of `self` in method 'bump' of class 'Counter' requires the method to be declared as `modifier`

Declare it pub modifier func bump(): void. (The message says self; it means the object the method was called on, which you write as this.)

Mistake: calling a modifier method outside safe

This code does not compile
@mode(host.scripted)

class Counter {
    pub var count: int = 0;

    pub modifier func bump(): void {
        this.count = this.count + 1;
    }
}

let hits = Counter();
hits.bump();
println(hits.count);
main.lpscr:12:1: error[E_SAFE_CONTEXT_REQUIRED]: E_SAFE_CONTEXT_REQUIRED: `modifier` method 'bump' called on ARC receiver of type 'Counter' outside `safe {}` or `safe func` context — wrap the call in `safe { ... }` or mark the enclosing function as `safe func`

"ARC receiver" means an ordinary, reference-counted object. Wrap the call, or put the code in a safe func:

class Counter {
    pub var count: int = 0;

    pub modifier func bump(): void {
        this.count = this.count + 1;
    }
}

safe func countWords(words: list[string]): Counter {
    let counter = Counter();
    for (word in words) {
        counter.bump();
    }
    return counter;
}

let hits = Counter();
safe {
    hits.bump();
}
println(hits.count);
println(countWords(["to", "be", "or"]).count);
1
3

Assigning a pub var field from outside the class follows the same rule: hits.count = 0; on its own is E_SAFE_CONTEXT_REQUIRED (the message calls the field a "modifier method 'count'"), and safe { hits.count = 0; } compiles.

Mistake: changing an object through a plain parameter

A parameter without a verb is reads: the function may look at the object but not change it.

This code does not compile
@mode(host.scripted)

class Counter {
    pub var count: int = 0;

    pub modifier func bump(): void {
        this.count = this.count + 1;
    }
}

func record(c: Counter): void {
    safe {
        c.bump();
    }
}
main.lpscr:13:9: error[E_READS_MUTATION]: E_READS_MUTATION: parameter 'c' is a `reads` borrow (a parameter without a verb is read-only, §3-2.4.9) and cannot be mutated (`modifier` method `bump` called on it); declare it `edits` and pass `edits: …` to change the caller's value, or copy it into a local first

Declare the parameter edits, make the function a safe func, and acknowledge the verb at the call site, as payInterest does in Listing 8-2:

safe func record(edits c: Counter): void {
    c.bump();
}

let hits = Counter();
safe {
    record(edits: hits);
    record(edits: hits);
}
println(hits.count);
2

The Language Reference says a function with an edits parameter is implicitly a safe func (§3-2.4.12). The 1.25 compiler still reports E_SAFE_CONTEXT_REQUIRED inside such a function unless it is declared safe func, so write safe func explicitly. Chapter 19 covers the verbs in depth.

Constructors

Generated constructors

A class that declares no new gets one generated from its fields. Arguments are matched to fields by name, in any order, and a field with a default value may be left out (ruling IMPCTOR-1):

class RetryPolicy {
    pub let attempts: int;
    pub let delayMs: int = 250;
    pub let backoff: float = 2.0;
}

let standard = RetryPolicy(attempts = 3);
let patient = RetryPolicy(backoff = 1.5, attempts = 10, delayMs = 1000);
println(standard);
println(patient);
RetryPolicy(attempts: 3, delayMs: 250, backoff: 2.0)
RetryPolicy(attempts: 10, delayMs: 1000, backoff: 1.5)

Leaving out a field that has no default is an error that names the field:

This code does not compile
@mode(host.scripted)

class RetryPolicy {
    pub let attempts: int;
    pub let delayMs: int = 250;
}

let policy = RetryPolicy(delayMs = 100);
main.lpscr:8:14: error[E0001]: missing required field 'attempts' in 'RetryPolicy' constructor call

Write your own new when construction needs to do more than copy arguments into fields — validate them, compute a field, or open a resource.

Static factories instead of overloaded constructors

In Java or C# you might give a class two constructors. A Loup class has at most one new:

This code does not compile
@mode(host.scripted)

class Endpoint {
    pub let host: string;
    pub let port: int;

    new(host: string, port: int) {
        this.host = host;
        this.port = port;
    }

    new(host: string) {
        this.host = host;
        this.port = 80;
    }
}
main.lpscr:12:5: error[E0001]: canonical name collision: 'main.lpscr::Endpoint::constructor' is already assigned to another callable
  note: previously assigned to callable with HirId(5001420)

The message comes from deep inside the compiler (the Language Reference names this error E_DUPLICATE_CONSTRUCTOR), but the cause is the second new. Give the alternatives names as static methods — static factories (§1-8.4). A factory can also return a Result when construction can fail, which new cannot:

class Endpoint {
    pub let host: string;
    pub let port: int;

    new(host: string, port: int) {
        this.host = host;
        this.port = port;
    }

    pub static func localhost(port: int = 8080): Endpoint {
        return Endpoint("localhost", port);
    }

    pub static func checked(host: string, port: int): Result[Endpoint, string] {
        if (port < 1 or port > 65535) {
            return Err(i"port {$ port $} is out of range");
        }
        return Ok(Endpoint(host, port));
    }

    pub func address(): string {
        return i"{$ this.host $}:{$ this.port $}";
    }
}

println(Endpoint.localhost().address());
println(Endpoint.localhost(port = 3000).address());

for (port in [443, 70000]) {
    match Endpoint.checked("api.example.com", port) {
        Ok(endpoint) => println(endpoint.address()),
        Err(message) => println(i"error: {$ message $}"),
    }
}

Listing 8-3: Named factories, one of them fallible

localhost:8080
localhost:3000
api.example.com:443
error: port 70000 is out of range

Visibility

Loup has no private or protected keyword. Visibility works like this (§1-8.2, ruling FPRIV-1):

Declaration Without pub With pub
A class field visible only inside the class and its subclasses visible everywhere
A method, function, class or other module-level name visible in the same file (module) visible to every module that imports it

So a field without pub is private to its class, even from other code in the same file:

This code does not compile
@mode(host.scripted)

class Wallet {
    pub let owner: string;
    var cents: int = 0;

    new(owner: string) {
        this.owner = owner;
    }

    pub modifier func add(amount: int): void {
        this.cents = this.cents + amount;
    }

    pub func balance(): int {
        return this.cents;
    }
}

let wallet = Wallet("Grace");
safe {
    wallet.add(250);
}
println(wallet.cents);
main.lpscr:24:9: error[E0001]: member 'cents' of class 'Wallet' is private

Read it through the public method instead — println(wallet.balance()); prints 250. Keeping cents private means every change to it goes through add, which is the one place you would add validation later.

§1-8.2 of the Language Reference still describes all non-pub members as module-private. Ruling FPRIV-1 made class fields private to the class, which is what both compilers enforce; methods stay module-private.

Static Members

A static field or method belongs to the class rather than to an object, and is reached through the class name:

class Ticket {
    pub static var issued: int = 0;
    pub let number: int;

    new() {
        Ticket.issued = Ticket.issued + 1;
        this.number = Ticket.issued;
    }

    pub static func clampPriority(value: int): int {
        if (value < 1) {
            return 1;
        }
        if (value > 5) {
            return 5;
        }
        return value;
    }
}

let a = Ticket();
let b = Ticket();
println(i"tickets {$ a.number $} and {$ b.number $}; {$ Ticket.issued $} issued");
println(Ticket.clampPriority(9));
tickets 1 and 2; 2 issued
5

Static methods have no this.

Inheritance

A class may extend one other class and implement any number of interfaces (Chapter 9). The subclass inherits the parent's fields and methods:

class Animal {
    pub let name: string;

    new(name: string) {
        this.name = name;
    }

    pub func speak(): string {
        return i"{$ this.name $} makes a sound";
    }

    pub func describe(): string {
        return i"I am {$ this.name $}";
    }
}

class Cat : Animal {
    pub let indoor: bool;

    new(name: string, indoor: bool) {
        super(name);
        this.indoor = indoor;
    }

    pub override func speak(): string {
        return i"{$ this.name $} says: Meow!";
    }
}

class Kitten : Cat {
    new(name: string) {
        super(name, true);
    }

    pub override func speak(): string {
        return super.speak() + " (quietly)";
    }
}

let animals: list[Animal] = [Animal("Generic"), Cat("Whiskers", false), Kitten("Tiny")];
for (animal in animals) {
    println(animal.speak());
}
println(Kitten("Bo").describe());

Listing 8-4: Single inheritance, super(...), override and super.method()

Generic makes a sound
Whiskers says: Meow!
Tiny says: Meow! (quietly)
I am Bo
  • class Cat : Animal makes Cat a subclass of Animal. A Cat can be used wherever an Animal is expected.
  • super(name) runs the parent's constructor. It must come before the constructor touches this.
  • override marks a method that replaces one the parent already implements (§1-8.3.1).
  • super.speak() calls the parent's version of the method, so Kitten extends Cat's behaviour instead of repeating it.
  • The loop calls speak() on values typed Animal, and each object runs its own class's version. That is dynamic dispatch.

A generated constructor includes the parent's fields, first in the argument list: with class Animal { pub let name: string; } and class Cat : Animal { pub let indoor: bool = true; }, Cat(name = "Tom") works.

Mistake: overriding without override

This code does not compile
@mode(host.scripted)

class Animal {
    pub func speak(): string {
        return "...";
    }
}

class Cat : Animal {
    pub func speak(): string {
        return "Meow";
    }
}
main.lpscr:10:5: error[E_OVERRIDE_REQUIRED]: method `speak` on class `Cat` shadows a parent method on `Animal` and must be marked `override` (E_OVERRIDE_REQUIRED)

The keyword guards against replacing a parent method by accident. Add it: pub override func speak(). The reverse mistake — override on a method the parent does not have — is E_OVERRIDE_NO_TARGET.

Mistake: forgetting super(...)

This code does not compile
@mode(host.scripted)

class Animal {
    pub let name: string;
    new(name: string) { this.name = name; }
}

class Cat : Animal {
    pub let indoor: bool;

    new(name: string, indoor: bool) {
        this.indoor = indoor;
    }
}
main.lpscr:12:9: error[E_SUPER_NOT_CALLED]: E_SUPER_NOT_CALLED: constructor of `Cat` accesses `this` before calling `super(...)`; `Animal` has a non-default constructor (§1-8.4)

Animal's constructor needs a name, and only Cat's constructor can supply it. Call super(name); as its first statement.

final

final class Money { … } cannot be extended (E_FINAL_EXTEND), and a pub final func cannot be overridden (E_FINAL_OVERRIDE). Use final on classes and methods whose behaviour other code relies on.

Abstract Classes

An abstract class is a base class that leaves some methods for its subclasses to supply. The classic use is a template method: the base class fixes the outline and calls an abstract step:

abstract class Report {
    pub let title: string;

    new(title: string) {
        this.title = title;
    }

    abstract func writeBody(): void;

    pub func show(): void {
        println(i"== {$ this.title $} ==");
        this.writeBody();
        println("== end ==");
    }
}

class SalesReport : Report {
    pub let orders: list[int];

    new(orders: list[int]) {
        super("Sales");
        this.orders = orders;
    }

    pub override func writeBody(): void {
        var total = 0;
        for (cents in this.orders) {
            total = total + cents;
        }
        println(i"{$ this.orders.length() $} orders, {$ total $} cents");
    }
}

class InventoryReport : Report {
    pub let items: dict[string, int];

    new(items: dict[string, int]) {
        super("Inventory");
        this.items = items;
    }

    pub override func writeBody(): void {
        for (name in this.items.keys()) {
            println(i"{$ name $}: {$ this.items[name] $}");
        }
    }
}

let reports: list[Report] = [
    SalesReport([1250, 899, 4000]),
    InventoryReport({"bolts": 120, "nuts": 75}),
];
for (report in reports) {
    report.show();
}

Listing 8-5: An abstract class with a template method

== Sales ==
3 orders, 6149 cents
== end ==
== Inventory ==
bolts: 120
nuts: 75
== end ==

The Language Reference makes override optional when implementing an abstract method; the 1.25 compiler requires it, so write it.

⚠ Planned. Abstract classes are only partly implemented in Loup 1.25. An abstract method that returns a value is rejected — abstract func area(): float; reports error[E0001]: method 'area' may not return a value on all code paths — and the compiler does not yet report E_ABSTRACT_INSTANTIATION (creating an abstract class) or E_ABSTRACT_NOT_IMPLEMENTED (a subclass that leaves an abstract method out), both specified in §1-8.3. Until then, use abstract methods that return void, and express a contract with return values as an interface (Chapter 9), which the compiler checks completely.

Equality

Objects compare by identity unless the class defines ==. To compare by contents, implement Equatable and write the operator:

class Point implements Equatable[Point] {
    pub let x: float;
    pub let y: float;

    new(x: float, y: float) {
        this.x = x;
        this.y = y;
    }

    pub operator func ==(other: Point): bool {
        return this.x == other.x and this.y == other.y;
    }
}

let a = Point(1.0, 2.0);
let b = Point(1.0, 2.0);
println(a == b);
println(a != Point(0.0, 0.0));
true
true

!= is derived from ==.

Mistake: an operator without its interface

This code does not compile
@mode(host.scripted)

class Point {
    pub let x: float;
    pub let y: float;

    new(x: float, y: float) {
        this.x = x;
        this.y = y;
    }

    pub operator func ==(other: Point): bool {
        return this.x == other.x and this.y == other.y;
    }
}
main.lpscr:12:5: error[E_OPERATOR_NO_INTERFACE]: E_OPERATOR_NO_INTERFACE: operator func '==' on 'Point' requires `implements Equatable` on the enclosing type

Every operator belongs to an interface (== to Equatable, + to Add, and so on; Chapter 9), and the class must say it implements it.

Known gap. Collections do not use a class's == yet. In 1.25, list.contains, dict keys and set elements compare objects by identity even when the class implements Equatable (and Hashable), so [Point(1.0, 2.0)].contains(Point(1.0, 2.0)) is false. Use strings, numbers or structs as keys, or search with a loop and ==.

Dynamic Classes

Most classes have a fixed set of fields. A dynamic class can also carry fields added at run time — useful for loosely structured data such as settings read from a file. Dynamic fields are reached with obj!{name}, never with a dot (§1-8.7):

dynamic class Settings {
    pub let app: string;

    new(app: string) {
        this.app = app;
    }
}

let settings = Settings("indexer");
settings!{timeout} = 5000;
settings!{"log-level"} = "debug";

let timeout: int = settings!{timeout};
println(timeout + 1);
println(settings!{"log-level"});
println(settings.hasField("retries"));
println(settings.fieldNames());
5001
debug
false
["timeout", "log-level"]

A name with characters that are not valid in an identifier goes in quotes: !{"log-level"}. Reading a field that was never set throws KeyError; check first with hasField. Assigning a dynamic read to a typed binding (let timeout: int = …) converts it from Any.

Mistake: reading a dynamic field with a dot

This code does not compile
@mode(host.scripted)

dynamic class Settings {
    pub let app: string;
    new(app: string) { this.app = app; }
}

let settings = Settings("indexer");
settings!{timeout} = 5000;
println(settings.timeout);
main.lpscr:10:9: error[E0001]: type 'Settings' has no member 'timeout'

A dot only ever reaches declared members. The different spelling is deliberate: settings.app is checked by the compiler, settings!{timeout} is looked up at run time. Write settings!{timeout}.

Known gap. §1-8.7 says a typed read such as let t: int = settings!{timeout} throws TypeError when the stored value has another type. In 1.25 the conversion is not checked: a string stored under timeout is bound to t unchanged.

Prefer a regular class or a struct whenever you know the fields in advance. In host.dual a dynamic class draws the warning W_ANY_IN_AOT_MODE, and in freestanding it is an error.

Releasing Resources

Class instances are reference-counted in every mode (§1-8.1.1): an object lives while at least one reference to it exists. Memory takes care of itself, but a file, a lock or a temporary directory should be released at a known point. Loup gives you two tools:

  • with and the Closeable interface: a class with a close() method that implements Closeable can be used in a with block, and close() runs when the block ends, even if it ends with an exception.
  • del x: ends the binding x and runs the object's finalize() method, if it has one (§1-6.11, §1-8.9).
class Session implements Closeable {
    pub let name: string;

    new(name: string) {
        this.name = name;
        println(i"open {$ name $}");
    }

    pub func close(): void {
        println(i"close {$ this.name $}");
    }
}

class ScratchFile {
    pub let path: string;

    new(path: string) {
        this.path = path;
        writeText(path, "scratch data");
    }

    pub func finalize(): void {
        println(i"finalize {$ this.path $}");
    }
}

with (session = Session("db")) {
    println(i"using {$ session.name $}");
}

let scratch = ScratchFile("scratch.tmp");
println(exists(scratch.path));
del scratch;
println("done");

Listing 8-6: with closes a Closeable; del runs finalize()

open db
using db
close db
true
finalize scratch.tmp
done

Known gap. Under the reference-counting contract, finalize() also runs when the last reference goes away on its own — for example, when a function that created the object returns. The 1.25 interpreter runs finalize() only for del. Use with or del whenever cleanup matters.

Because objects are reference-counted, two objects that refer to each other keep each other alive. Break such cycles with a weak field; Chapter 19 covers weak, with and defer together.

Classes That Scripts Cannot Use

unique class

A unique class has instances that can never be shared, only moved. It belongs to the native and freestanding modes:

This code does not compile
@mode(host.scripted)

unique class Token {
    pub let id: int;
    new(id: int) { this.id = id; }
}
main.lpscr:3:1: error[E_UNIQUE_CLASS_ARC_BINDING]: E_UNIQUE_CLASS_ARC_BINDING: `unique class 'Token'` is only valid in `@mode(native)` or `@mode(freestanding)`

(The message uses the retired name native; the mode is host.native.) In a script, use an ordinary class, or a unique let binding (Chapter 19). The Loup Native Book covers unique class.

let fields are not yet enforced

A let field is meant to be fixed after construction. Loup 1.25 does not enforce that yet (candidate ruling FIELD-2):

This code does not do what it should
class Account {
    pub let id: string;

    new(id: string) {
        this.id = id;
    }

    pub func rename(newId: string): void {
        this.id = newId;
    }
}

let account = Account("A-1");
account.rename("B-2");
println(account.id);
B-2

The method assigns a let field and is not even a modifier; the compiler accepts it. Treat let fields as fixed anyway — a future release will reject this code.

⚠ Planned. Annotations such as @deprecated("use fetchJson") are specified to produce a warning wherever the marked declaration is used (§1-8.8). In 1.25 the compiler warns Unknown annotation '@deprecated' at the declaration and nothing at uses. Reading annotations at run time belongs to core.reflect (Chapter 14), which is not implemented.

Summary

  • A class declares fields with let or var (pub to expose them), at most one new constructor, and methods that reach the object through this..
  • Objects are shared by reference; == compares identity unless the class implements Equatable and defines operator func ==.
  • A method that assigns a var field is modifier; calling it, or assigning a pub var field from outside, needs safe { … } or a safe func; a function that changes an object it receives takes it as edits.
  • Without new, the generated constructor takes the fields by name, parent fields included; defaulted fields may be left out.
  • Alternative constructors are static factories, which may return Result.
  • Non-pub fields are private to the class and its subclasses; non-pub methods are private to the file.
  • Inheritance is single: super(args) in the constructor, override on replaced methods, super.m() to call the parent's version, final to forbid extension.
  • Abstract classes work for void abstract methods today; use interfaces for contracts that return values.
  • dynamic class adds run-time fields reached with !{name}.
  • with + Closeable and del + finalize() release resources at a known point.

Try It Yourself

  1. Write a RateLimiter class with a pub var list of request timestamps and a modifier method allow(now: int): bool that permits at most three requests in any ten-second window. Show that two limiters do not share state.
  2. Give Endpoint (Listing 8-3) a factory fromText(text: string): Result[Endpoint, string] that splits "host:port", and print both a good and a bad input.
  3. Extend Listing 8-4 with a Dog : Animal class. Then remove override from its speak() and read the error.
  4. Write a TempDir class that implements Closeable, creates a file in its constructor and prints a message in close(). Use it in a with block that throws, catch the exception outside, and confirm that close() ran first.