Chapter 9: Classes and Objects
A struct (Chapter 7) is a value: two Point(x = 3, y = 4) are the same point, and assigning one copies it. A class describes an object with identity: two shopping carts holding the same items are still two carts, and assigning a cart to a second name gives you a second name for the same cart.
This chapter covers declaring classes, constructors, the modifier/safe rules for changing an object's state, inheritance, runtime type checks, static members, the ownership verbs on class parameters, cleanup with del, weak fields, and generic classes. The normative text is in the Language Reference §1-8 (classes) and §3-2.4.9–§3-2.4.13 (ownership verbs, modifier, safe, synchronized).
How class instances are managed. Every Loup mode presents ARC semantics for class instances: an instance is shared by reference and released when its last strong reference goes away (Language Reference §1-8.1.1). That holds for web.compiled and web.scripted too. On the Wasm target, instances are host-managed references, not Wasm GC objects; Wasm GC is a possible future implementation strategy, not what Loup 1.25 emits (Language Reference §3-1). If you have read older Loup material that said "web code is garbage-collected, so cycles are free", that is not how Loup works today; see "Reference cycles and weak" below.
Declaring a class
@mode(web.compiled)
class Counter {
pub var count: int;
new(initial: int) {
this.count = initial;
}
pub modifier func increment(): void {
this.count = this.count + 1;
}
pub func get(): int {
return this.count;
}
}
func main(): void {
let clicks = Counter(0);
safe {
clicks.increment();
clicks.increment();
}
println(clicks.get());
let alias = clicks; # a second name for the same object
safe { alias.increment(); }
println(clicks.get());
println(clicks);
}
Listing 9-1: A class with a constructor, a mutating method, and a read-only method
2
3
Counter(count: 3)
The pieces:
class Counter { … }declares the class. With no:clause it extendsAny, the root of every class hierarchy.pub var count: int;is a field. Every class field is declared withlet(assigned once, in the constructor) orvar(reassignable). Fields are private to the class unless markedpub.new(initial: int) { … }is the constructor. It has no return type, and a class has at most one. Inside it,thisis the object being built. Writethis.count, not a barecount.pub modifier func increment()writes avarfield, so it must be declaredmodifier. Calling amodifiermethod on an ordinary class instance must happen insidesafe { … }or asafe func. The rule and its reasons are below.pub func get()only reads, so it needs neither.let alias = clicks;copies the reference, not the object.alias.increment()changes the object both names point at. Compare Chapter 7, wherelet b = a;on a struct made an independent copy.println(clicks)renders the object structurally: the class name and each field.
Fields need let or var
If you come from TypeScript, a bare count: int; looks like a field declaration. In Loup it is an error:
@mode(web.compiled)
class Counter {
count: int;
}
func main(): void {
println("never runs");
}
main.lpweb:4:5: error[E0001]: E_BARE_FIELD: class field `count` must be declared `let` or `var`; write `var count: ...` (mutable) or `let count: ...` (immutable)
The compiler wants you to say whether the field can change after construction. Pick var for a counter:
@mode(web.compiled)
class Counter {
pub var count: int = 0;
}
func main(): void {
println(Counter().count);
}
0
A field with a default value (= 0) does not need to be set in a constructor. This class has no new at all, so Loup generates a constructor for it (see "Constructors" below).
Fields are private unless pub
A field without pub can be read and written only inside the class's own methods and those of its subclasses (ruling FPRIV-1). A function in the same file cannot reach it:
@mode(web.compiled)
class Account {
var balance: int = 0;
}
func main(): void {
let a = Account();
println(a.balance);
}
main.lpweb:9:13: error[E0001]: member 'balance' of class 'Account' is private
Either mark the field pub, or keep it private and expose a method. The second is usually better for state you want to protect:
@mode(web.compiled)
class Account {
var balance: int = 0;
pub func current(): int {
return this.balance;
}
}
func main(): void {
let a = Account();
println(a.current());
}
0
Methods and module-level declarations without pub are private to the module (file) instead; there is no protected, pub(package) or pub(module) (Language Reference §1-8.2).
Constructors
A class with an explicit new(...) is built by calling the class name with that constructor's arguments, positionally or by name: Counter(0) or Counter(initial = 0).
A class with no new gets a generated constructor that takes each field by name, including inherited fields (ruling IMPCTOR-1). Fields with a default are optional:
@mode(web.compiled)
class Account {
pub let owner: string;
pub var balance: int = 0;
}
class Savings : Account {
pub let rate: float = 0.02;
}
func main(): void {
let checking = Account(owner = "ann");
let savings = Savings(owner = "bo", balance = 100);
println(checking);
println(savings);
}
Listing 9-2: Generated constructors fill fields by name, inherited fields included
Account(owner: "ann", balance: 0)
Savings(owner: "bo", balance: 100, rate: 0.02)
Leaving out a field that has no default is an error, because the object would be incomplete:
@mode(web.compiled)
class Account {
pub let owner: string;
pub var balance: int = 0;
}
func main(): void {
let a = Account(balance = 5);
println(a);
}
main.lpweb:9:13: error[E0001]: missing required field 'owner' in 'Account' constructor call
Pass owner = "…", or give the field a default.
Changing an object's state: modifier and safe
Because class instances are shared by reference, a method that changes a field changes it for every holder of that reference. Loup makes such methods visible in two places: on the declaration (modifier) and at the call (safe). The rules are in the Language Reference §3-2.4.11 and §3-2.4.12.
First, a method that writes a var field must say so:
@mode(web.compiled)
class Counter {
pub var count: int = 0;
pub func increment(): void {
this.count = this.count + 1;
}
}
func main(): void {
let c = Counter();
c.increment();
println(c.count);
}
main.lpweb:7:9: error[E_MODIFIER_REQUIRED]: E_MODIFIER_REQUIRED: writing to `var` field 'count' of `self` in method 'increment' of class 'Counter' requires the method to be declared as `modifier`
Add modifier, and the compiler moves on to the call site:
@mode(web.compiled)
class Counter {
pub var count: int = 0;
pub modifier func increment(): void {
this.count = this.count + 1;
}
}
func main(): void {
let c = Counter();
c.increment();
println(c.count);
}
main.lpweb:13:5: error[E_SAFE_CONTEXT_REQUIRED]: E_SAFE_CONTEXT_REQUIRED: `modifier` method 'increment' 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`
A safe context is where Loup checks that a mutation does not happen through a reference somebody else is also using. You provide one in either of two ways:
@mode(web.compiled)
class Counter {
pub var count: int = 0;
pub modifier func increment(): void {
this.count = this.count + 1;
}
}
safe func incrementTwice(edits c: Counter): void {
c.increment();
c.increment();
}
func main(): void {
let c = Counter();
safe { c.increment(); } # 1. a safe block around the call
incrementTwice(edits: c); # 2. a safe func, which takes the requirement on itself
println(c.count);
}
Listing 9-3: Calling a modifier method from a safe block and from a safe func
3
The same rule covers assigning a pub var field from outside the class: c.count = 5; must also be inside safe { … }. Stage0's message for that case calls the field a "modifier method", but the fix is the same.
Not checked yet. Inside a
safecontext, the Language Reference also forbids calling amodifierwhile another alias of the receiver is live (E_SAFE_ALIAS_MUTABLE). Loup 1.25 does not check that yet: in Listing 9-1,alias.increment()compiles even thoughclicksis still in scope. Treat the rule as a design constraint you follow by hand.
Inheritance
A class extends at most one parent class, written after a colon. The subclass constructor calls the parent's constructor with super(...), and a method that replaces a parent method is marked override:
@mode(web.compiled)
class Shape {
pub let name: string;
new(name: string) {
this.name = name;
}
pub func area(): float {
return 0.0;
}
pub func describe(): string {
return i"{$ this.name $} with area {$ this.area() $}";
}
}
class Circle : Shape {
pub let radius: float;
new(radius: float) {
super("circle");
this.radius = radius;
}
pub override func area(): float {
return 3.14159 * this.radius * this.radius;
}
}
class Rect : Shape {
pub let w: float;
pub let h: float;
new(w: float, h: float) {
super("rect");
this.w = w;
this.h = h;
}
pub override func area(): float {
return this.w * this.h;
}
}
func main(): void {
let shapes: list[Shape] = [Circle(1.0), Rect(2.0, 3.0)];
for (s in shapes) {
println(s.describe());
}
}
Listing 9-4: A small shape hierarchy with virtual dispatch
circle with area 3.14159
rect with area 6
describe() is written once, in Shape, and calls this.area(). Dispatch is virtual: each object runs its own class's area(), even through a variable of type Shape.
Leave out override and the compiler refuses, because an accidental name clash with a parent method is a common source of bugs:
@mode(web.compiled)
class Shape {
pub func area(): float {
return 0.0;
}
}
class Square : Shape {
pub let side: float = 1.0;
pub func area(): float {
return this.side * this.side;
}
}
func main(): void {
println(Square().area());
}
main.lpweb:12:5: error[E_OVERRIDE_REQUIRED]: method `area` on class `Square` shadows a parent method on `Shape` and must be marked `override` (E_OVERRIDE_REQUIRED)
Writing pub override func area() fixes it. The reverse mistake, override on a method with nothing to override, is E_OVERRIDE_NO_TARGET.
Two more inheritance tools:
super.method(args)calls the parent's version from inside an override.final classcannot be extended (E_FINAL_EXTEND), andfinal funccannot be overridden.
@mode(web.compiled)
class Logger {
pub func format(msg: string): string {
return "[log] " + msg;
}
}
class TimedLogger : Logger {
pub override func format(msg: string): string {
return "12:00 " + super.format(msg);
}
}
func main(): void {
let log: Logger = TimedLogger();
println(log.format("cart saved"));
}
12:00 [log] cart saved
⚠ Planned. The Language Reference (§1-8.3) lets an
abstract classdeclare body-less methods (abstract func area(): float;) that subclasses implement withoutoverride. Stage0 1.25.1 rejects the body-less declaration ("method 'area' may not return a value on all code paths"). Until it lands, give the base method a body as in Listing 9-4, or describe the contract with an interface (Chapter 10).
Checking an object's runtime type: is and as?
x is T asks whether x's runtime class is T or a subclass of it. The Language Reference (§1-4.7) says the compiler then narrows x to T inside the if. Loup 1.25 does not narrow yet, so the TypeScript-style code below fails:
@mode(web.compiled)
func label(s: Shape): string {
if (s is Circle) {
return i"circle, r = {$ s.radius $}";
}
return s.name;
}
func main(): void {
println(label(Circle(2.5)));
}
main.lpweb:5:33: error[E0001]: type 'Shape' has no member 'radius'
Inside the if, s still has the static type Shape. Convert it explicitly with as?, which gives Some(value) when the runtime type matches and None otherwise:
@mode(web.compiled)
class Shape {
pub let name: string;
new(name: string) { this.name = name; }
}
class Circle : Shape {
pub let radius: float;
new(radius: float) { super("circle"); this.radius = radius; }
}
class Rect : Shape {
new() { super("rect"); }
}
func label(s: Shape): string {
match s as? Circle {
Some(c) => { return i"circle, r = {$ c.radius $}"; },
None => { return s.name; },
}
}
func main(): void {
println(label(Circle(2.5)));
println(label(Rect()));
println(Rect() is Shape);
}
Listing 9-5: as? turns a runtime type check into an Option
circle, r = 2.5
rect
true
Two cautions for Loup 1.25:
- The Language Reference treats
instanceofas a synonym ofis. In Stage0 1.25.1,instanceofin an expression crashes the parser (a Rust panic, exit status 101). Writeis. x as T(without?) is specified to throwTypeErroron a mismatch. Stage0 does not check: the cast succeeds and the program fails later, at the first field access, with an internal interpreter error. Useas?and handleNone.
Static members
static fields and methods belong to the class rather than to an instance. A common use is a named factory; another is a per-class counter:
@mode(web.compiled)
class Ticket {
static var issued: int = 0;
pub let number: int;
new() {
Ticket.issued = Ticket.issued + 1;
this.number = Ticket.issued;
}
pub static func count(): int {
return Ticket.issued;
}
}
class Temperature {
pub static let FREEZING_K: float = 273.15;
pub let kelvin: float;
new(kelvin: float) {
this.kelvin = kelvin;
}
pub static func fromCelsius(c: float): Temperature {
return Temperature(c + Temperature.FREEZING_K);
}
pub func celsius(): float {
return this.kelvin - Temperature.FREEZING_K;
}
}
func main(): void {
let a = Ticket();
let b = Ticket();
println(i"tickets {$ a.number $} and {$ b.number $}, {$ Ticket.count() $} issued");
let room = Temperature.fromCelsius(21.5);
println(room.kelvin);
}
Listing 9-6: A static counter and a static factory method
tickets 1 and 2, 2 issued
294.65
Static members are reached through the class name: Ticket.count(), Temperature.FREEZING_K.
Ownership verbs on class parameters
A parameter's verb says what a function may do with an argument (Language Reference §3-2.4.9). The same four verbs apply to every type; with classes they matter most, because the argument is a reference to an object the caller still holds.
| Verb | The function may | Caller writes |
|---|---|---|
reads (the default when no verb is written) |
read the object | f(cart) |
edits |
change the object | f(edits: cart) |
takes |
keep or consume it; the caller may not use it afterwards | f(takes: cart) |
shares |
keep another reference to it | f(shares: cart) |
@mode(web.compiled)
class Cart {
pub var items: list[string] = [];
pub var total: int = 0;
pub modifier func add(name: string, cents: int): void {
this.items.push(name);
this.total = this.total + cents;
}
pub func count(): int {
return this.items.len();
}
}
func summary(cart: Cart): string { # reads, implicitly
return i"{$ cart.count() $} items, {$ cart.total $} cents";
}
safe func addGiftWrap(edits cart: Cart): void {
cart.add("gift wrap", 300);
}
func checkout(takes cart: Cart): int {
return cart.total;
}
func main(): void {
let cart = Cart();
safe {
cart.add("book", 1500);
cart.add("pen", 200);
}
println(summary(cart));
addGiftWrap(edits: cart);
println(summary(cart));
let paid = checkout(takes: cart);
println(i"paid {$ paid $}");
}
Listing 9-7: reads, edits and takes on a class parameter
2 items, 1700 cents
3 items, 2000 cents
paid 2000
Each verb has a mistake that goes with it. The listings below use the Cart class from Listing 9-7 (hidden at the end of each listing).
Changing an object you only reads. A parameter with no verb is read-only:
@mode(web.compiled)
func applyCoupon(cart: Cart): void {
safe { cart.add("coupon", -100); }
}
func main(): void {
let cart = Cart();
applyCoupon(cart);
println(cart.total);
}
main.lpweb:4:12: error[E_READS_MUTATION]: E_READS_MUTATION: parameter 'cart' is a `reads` borrow (a parameter without a verb is read-only, §3-2.4.9) and cannot be mutated (`modifier` method `add` called on it); declare it `edits` and pass `edits: …` to change the caller's value, or copy it into a local first
The message tells you the fix: declare the parameter edits cart: Cart and call applyCoupon(edits: cart).
Forgetting the acknowledgement at the call site. With edits declared, a plain addGiftWrap(cart) is refused. The caller has to write the verb too, so that a reader of main can see which calls change cart:
@mode(web.compiled)
safe func addGiftWrap(edits cart: Cart): void {
cart.add("gift wrap", 300);
}
func main(): void {
let cart = Cart();
addGiftWrap(cart);
println(cart.total);
}
main.lpweb:9:17: error[E_VERB_REQUIRED]: E_VERB_REQUIRED: parameter 'cart' requires 'edits' verb acknowledgment at call site
Write addGiftWrap(edits: cart);.
edits without safe func. The Language Reference (§3-2.4.12) says a function with an edits parameter is implicitly a safe func. Stage0 1.25.1 does not apply that rule yet, so leaving off safe fails at the first modifier call inside:
@mode(web.compiled)
func addGiftWrap(edits cart: Cart): void {
cart.add("gift wrap", 300);
}
func main(): void {
let cart = Cart();
addGiftWrap(edits: cart);
println(cart.total);
}
main.lpweb:4:5: error[E_SAFE_CONTEXT_REQUIRED]: E_SAFE_CONTEXT_REQUIRED: `modifier` method 'add' called on ARC receiver of type 'Cart' outside `safe {}` or `safe func` context — wrap the call in `safe { ... }` or mark the enclosing function as `safe func`
Write safe func addGiftWrap(...), as in Listing 9-7. That spelling is also correct under the specified rule, so it will keep working.
Using an object after takes. takes hands the object over. The caller's binding is consumed, and using it again is an error:
@mode(web.compiled)
func checkout(takes cart: Cart): int {
return cart.total;
}
func main(): void {
let cart = Cart();
let paid = checkout(takes: cart);
println(paid);
println(cart.total);
}
main.lpweb:11:13: error[E_USE_AFTER_TAKE]: E_USE_AFTER_TAKE: binding 'cart' was consumed by a `takes` parameter and cannot be used again
Read everything you need from the cart before handing it to checkout, or have checkout return what you need afterwards (here, it already returns the total).
synchronized classes
A synchronized class guards every instance with a runtime lock, so its modifier methods can be called without a safe context (Language Reference §3-2.4.13). In the browser, Loup code runs on one thread, so the lock matters only for code that is shared with multi-threaded host builds. It is shown here because you will meet it in libraries:
@mode(web.compiled)
synchronized class HitCounter {
var hits: int = 0;
pub modifier func hit(): void {
this.hits = this.hits + 1;
}
pub modifier func total(): int { # even a read of a `var` field takes the lock
return this.hits;
}
}
func main(): void {
let page = HitCounter();
page.hit(); # no safe {} needed: the lock provides exclusivity
page.hit();
println(page.total());
}
2
Inside a synchronized class, every method that touches a var field, even only to read it, must be a modifier, so that the access happens under the lock. A plain func total() here is rejected with E_MODIFIER_REQUIRED ("reading var field 'hits' … is unsynchronized"). For the same reason, keep the mutable fields private: the compiler warns (E_PUB_VAR_ON_SYNCHRONIZED) about a pub var, because reading or writing it from outside would bypass the lock.
unique values in web code
unique marks a value with exactly one owner: assigning it moves it (Language Reference §3-2.4.7). The Language Reference allows both unique let bindings and whole unique class declarations in every mode, but Stage0 1.25.1 accepts unique class only in host.native and freestanding:
@mode(web.compiled)
unique class Session {
pub let user: string;
new(user: string) { this.user = user; }
}
func main(): void {
unique let s = Session("ann");
println(s.user);
}
main.lpweb:3:1: error[E_UNIQUE_CLASS_ARC_BINDING]: E_UNIQUE_CLASS_ARC_BINDING: `unique class 'Session'` is only valid in `@mode(native)` or `@mode(freestanding)`
(The message still spells the native mode by its retired name; the current name is host.native.) In web code, declare an ordinary class and put unique on the binding instead:
@mode(web.compiled)
class Session {
pub let user: string;
new(user: string) { this.user = user; }
}
func main(): void {
unique let s = Session("ann");
let current = s; # moves the session; `s` is no longer usable
println(current.user);
}
ann
Reading s.user after the move would be E_USE_AFTER_MOVE. Chapter 20 returns to moves and borrowing.
Cleanup: del and finalize()
A class that holds something that must be released (a connection, a subscription, a timer) declares a finalize() method. Declaring it makes the class Finalizable. del x runs finalize() at that point and ends the binding (Language Reference §1-6.11, §1-8.9):
@mode(web.compiled)
class Subscription {
pub let topic: string;
new(topic: string) {
this.topic = topic;
println(i"subscribed to {$ topic $}");
}
pub func finalize(): void {
println(i"unsubscribed from {$ this.topic $}");
}
}
func main(): void {
let prices = Subscription("prices");
println("rendering prices");
del prices;
println("done");
}
Listing 9-8: del runs finalize() deterministically
subscribed to prices
rendering prices
unsubscribed from prices
done
After del, the name is gone:
@mode(web.compiled)
class Subscription {
pub let topic: string;
new(topic: string) { this.topic = topic; }
pub func finalize(): void { println("unsubscribed"); }
}
func main(): void {
let prices = Subscription("prices");
del prices;
println(prices.topic);
}
main.lpweb:12:13: error[E_USE_AFTER_DEL]: E_USE_AFTER_DEL: use of binding 'prices' after `del` — the binding is uninitialized once it has been passed to `del` (§1-6.11)
Under the specified ARC rules, finalize() also runs when the last strong reference goes away, for example when a function that created the object returns. On the Loup 1.25 interpreter it does not: an object that goes out of scope without del is never finalized. Call del for any cleanup that must happen. (A with block, Chapter 5, also gives scope-bound cleanup, but only for types that implement Scoped[T].)
Reference cycles and weak
Under ARC semantics, two objects that hold strong references to each other keep each other alive. A tree whose children point back to their parent is the usual example. The specified fix is to make the back-pointer weak: it does not keep its target alive, and reads as None once the target is released (Language Reference §1-8).
@mode(web.compiled)
class TreeNode {
pub let label: string;
pub var children: list[TreeNode] = [];
pub weak var parent: TreeNode?;
new(label: string) {
this.label = label;
}
}
func main(): void {
let menu = TreeNode("menu");
let item = TreeNode("settings");
safe {
item.parent = Some(menu);
menu.children.push(item);
}
println(menu.children.len());
match item.parent {
Some(p) => println(i"parent: {$ p.label $}"),
None => println("parent released"),
}
}
Listing 9-9: A weak back-pointer from child to parent
1
parent: menu
The weak field has an optional type (TreeNode?) and is assigned Some(menu).
What works and what does not in web modes today:
weakfields compile and run inweb.compiledandweb.scripted, on the interpreter and in a--target browserWasm build. The Language Reference disagrees with itself here: the mode matrix (§1-2.6) listsweakfor both web modes, while §3-1.2 says the Wasm target has no zeroing side table for it. Follow the matrix, but note the next point.- On the interpreter, a
weakfield is not cleared when its target's last strong reference goes away; it behaves like a strong reference. Writeweakwhere the design calls for it, and do not rely on readingNone. - Closure capture lists (
func() [weak self] { … }) are refused in web modes: "capture lists are only allowed in @compiled files targeting --target native".
⚠ Planned.
arc let/arc var(advisory pinning, Language Reference §1-8) are in the mode matrix for web modes but do not parse in Loup 1.25 (error[E0003]: expected ';').
Generic classes
A class can take type parameters in square brackets, like the built-in list[T] and dict[K, V]:
@mode(web.compiled)
class Stack[T] {
var items: list[T] = [];
pub modifier func push(value: T): void {
this.items.push(value);
}
pub modifier func pop(): Option[T] {
if (this.items.len() == 0) {
return None;
}
return Some(this.items.pop());
}
pub func size(): int {
return this.items.len();
}
}
func main(): void {
let undo: Stack[string] = Stack();
safe {
undo.push("type 'a'");
undo.push("type 'b'");
undo.push("delete line");
}
println(undo.size());
safe {
println(undo.pop().unwrap_or("nothing to undo"));
println(undo.pop().unwrap_or("nothing to undo"));
}
println(undo.size());
let empty: Stack[int] = Stack();
safe { println(empty.pop().unwrap_or(-1)); }
}
Listing 9-10: A generic undo stack
3
delete line
type 'b'
1
-1
Give the binding its full type (let undo: Stack[string] = Stack();). In Stage0 1.25.1, explicit type arguments on the constructor call (Stack[string]()) are not applied: the call type-checks against an unresolved parameter and fails with messages such as expected 'T0', found 'string'. Chapter 11 covers type parameters and their bounds.
A worked example: an observable store
Here is a class you might write in a web app: a store that holds a value and notifies listeners when it changes. It combines a generic class, a list of function values, modifier methods and a closure that updates an outer variable, which must declare it nonlocal (closures are covered in Chapter 4).
@mode(web.compiled)
class Store[T] {
var value: T;
var listeners: list[fn(T): void] = [];
new(initial: T) {
this.value = initial;
}
pub func get(): T {
return this.value;
}
pub modifier func subscribe(listener: fn(T): void): void {
this.listeners.push(listener);
}
pub modifier func set(next: T): void {
this.value = next;
for (listener in this.listeners) {
listener(next);
}
}
}
func main(): void {
let cartSize: Store[int] = Store(0);
var renders = 0;
safe {
cartSize.subscribe(func(n: int): void {
println(i"badge shows {$ n $}");
});
cartSize.subscribe(func(n: int): void {
nonlocal renders;
renders = renders + 1;
});
cartSize.set(1);
cartSize.set(3);
}
println(i"current: {$ cartSize.get() $}, renders: {$ renders $}");
}
Listing 9-11: A generic observable store
badge shows 1
badge shows 3
current: 3, renders: 2
The store has identity (every part of the app that holds it sees the same value), its listener list is changed in place, and every change goes through a modifier method. That is the profile of a class rather than a struct. The reactive signal type planned for browser UI (Chapter 16) follows the same pattern.
Try it yourself
- Add a
pub modifier func remove(name: string, cents: int)toCartand asafe functhat applies a discount through aneditsparameter. Then call it withoutedits:and read the diagnostic. - Give
Shapeaperimeter()method and override it inCircleandRect. Remove oneoverridekeyword and read the error. - Add an
unsubscribetoStorethat removes the most recently added listener, and check withrendersthat it stops being called.
Summary
- A class has identity; assigning or passing an instance shares the same object. Every mode, the web modes included, presents ARC semantics for class instances.
- Fields are declared with
letorvar(a bare field isE_BARE_FIELD) and are private to the class unlesspub. new(...)is the constructor; without one, Loup generates a constructor that takes the fields by name.- A method that writes a field is a
modifier, and calling it on an ordinary instance requiressafe { … }or asafe func. - Single inheritance with
: Parent,super(...),super.method(), and mandatoryoverride. Body-lessabstractmethods are planned. - Use
isandas?for runtime type checks. Loup 1.25 does not narrow afteris, andinstanceofcrashes the Stage0 parser. - Parameter verbs:
readsby default,editsandtakesacknowledged at the call site. Stage0 needs an explicitsafe funcfor a function that mutates throughedits. delrunsfinalize()deterministically; the interpreter does not finalize objects that simply go out of scope.weakfields compile in web modes but are not cleared on the interpreter; capture lists andarc letare not available.- Generic classes take
[T]; annotate the binding (let s: Stack[int] = Stack();).