Classes
Declaring a Class
Section titled “Declaring a Class”Each class lives in its own .sau file. Fields are declared at the top, followed by an fn init method (the constructor) and the rest of the methods.
export class Player local name: string local health: integer local speed: float
static maxHealth: integer = 100
fn init(name: string, health: integer, speed: float) self.name = name self.health = health self.speed = speed end
fn greet() print("Hi, I am " .. self.name) end
fn damage(amount: integer) self.health = self.health - amount end
fn isAlive() -> boolean return self.health > 0 end
local fn secret() print("This is private") end
static fn getMaxHealth() -> integer return maxHealth endendfn init is the only constructor — there is no constructor keyword.
Instantiation
Section titled “Instantiation”Call the class as if it were a function:
class Player name: string = "" health: integer = 0 score: integer = 0
fn init(name: string, health: integer, score: integer) self.name = name self.health = health self.score = score end
fn greet() println("Hi, I am " .. self.name) endend
local p: Player = Player("Arthur", 100, 5)
p.greet() -- Hi, I am ArthurImplicit self
Section titled “Implicit self”Inside a method body, self is always in scope if it is a non static and in an instance method (and init), so self is the instance.
In addition, every class member — static fields, static methods, instance methods — is reachable by its bare name from inside any method of the same class. Local variables and parameters can shadow them, which is what you want.
class Counter local count: integer = 0
static local cap: integer = 10
fn tick() if self.count >= cap then return end
self.count = self.count + 1 self.report() end
fn report() print("Count is " .. self.count) endendAccess Modifiers
Section titled “Access Modifiers”| Syntax | Access |
|---|---|
name: string |
public field |
local name: string |
private field |
fn method() |
public method |
local fn method() |
private method |
static field: T = value |
class-level, shared, public |
static local field: T = value |
class-level, shared, private |
Field Initialization
Section titled “Field Initialization”The rule for locals holds for fields too: a non-nullable field is never allowed to start out nil. Every field must therefore get its value from one of three places — a default in the declaration, an assignment in init, or a ? on its type:
class Player local name: string = "anon" -- ok: default local level: integer -- ok: `init` assigns it local clan: string? -- ok: nullable, starts nil
fn init(level: integer) self.level = level endendLeave all three off and the field is reported at compile time:
class Player local level: integer -- ERROR: never initializedendThat covers a class with no init at all (there is nowhere to assign the field) as well as an init that forgets one.
Static fields are stricter — nothing runs before the first read of a static, so init is not an option and the value has to be in the declaration:
class Board static local scores: table<integer> = {} -- ok static local pending: table<integer>? -- ok, starts nil static local totals: table<integer> -- ERROR: never initializedendStatic Members
Section titled “Static Members”Static fields and methods belong to the class itself, not to instances. They are accessed via the class name from the outside, or by bare name (or self.name in a static fn) from inside:
class Player static maxHealth: integer = 100
static fn getMaxHealth() -> integer return Player.maxHealth endend
println(Player.maxHealth) -- 100println(Player.getMaxHealth()) -- 100Static fields are shared across all instances. Modifying them affects the class globally:
class Player static maxHealth: integer = 100end
Player.maxHealth = 200println(Player.maxHealth) -- 200A class with no fn init promotes every private local field = expr to a static, evaluated once at class-declaration time. This makes a class usable as a small module:
class Person name: string = ""
fn init(name: string) self.name = name end
fn introduce() println("I am " .. self.name) endend
class Main static local lauris: Person = Person("Laurynas")
static fn main() lauris.introduce() -- `lauris` resolves via the class endend
Main.main() -- I am LaurynasThe word local is doing the work there. A public defaulted field is an
ordinary instance field whose default runs at construction, so it is per
instance and writable like any other:
class Counter n: integer = 0 -- one per instance local built: integer = 0 -- one, shared, promoted to a staticend
local a: Counter = Counter()local b: Counter = Counter()a.n = 5 -- ok; `b.n` is still 0Adding or removing an init therefore changes nothing about the public
fields — only about the private ones, which have nowhere else to be
initialized.
Inheritance
Section titled “Inheritance”Use extends to inherit from another class. Call the parent’s init with self.super(...) from inside init:
export class Entity name: string
fn init(name: string) self.name = name end
fn getName() -> string return self.name endend
export class Player extends Entity local health: integer local speed: float
fn init(name: string, health: integer, speed: float) self.super(name)
self.health = health self.speed = speed end
fn greet() print("Hi, I am " .. self.getName()) endendRules:
- A class can only extend one parent
- Private members from the parent are not accessible in the child
- Public and static members are inherited
Overriding
Section titled “Overriding”Redeclaring an inherited method overrides it. The override has to be usable everywhere the parent’s version was, because a caller holding the parent type cannot know which one it will get. The checker enforces:
- Same parameter count and types. Widening or renaming the shape of the call is a compile error.
- A return type the parent’s callers accept. Narrowing is fine — an override may return a subclass of what the parent declared. Returning something unrelated is a compile error.
- Instance stays instance, static stays static.
class Base fn get() -> integer return 1 endend
class Derived extends Base fn get() -> string -- ERROR: the parent returns `integer` return "oops" endendfn init is exempt — constructors aren’t dispatched through a parent
reference, so a subclass constructor may take whatever parameters it needs
and forward what the parent wants via self.super(...).
If a method wasn’t meant to override anything, give it a different name.
Generic Classes
Section titled “Generic Classes”A class takes type parameters after its name, and they are in scope for every field, method signature and body inside it:
class Box<T> value: T
fn init(value: T) self.value = value end
fn get() -> T return self.value endend
local ints: Box<integer> = Box(5)local n: integer = ints.get() -- `T` is `integer` here
local words: Box<string> = Box("hi")local s: string = words.get() -- and `string` hereThe argument is inferred from the constructor when you don’t write it, so local b = Box(5) gives a Box<integer> and b.get() + 1 type-checks. Several parameters bind independently, each from the position it appears in:
class Pair<A, B> left: A right: B
fn init(left: A, right: B) self.left = left self.right = right end
fn first() -> A return self.left endend
local p = Pair(7, "seven") -- Pair<integer, string>local n: integer = p.first()Type arguments are invariant, the same rule table elements follow and for the same reason: a Box<string> accepted into a Box<integer> slot would be an alias through which the wrong type could be written back.
class Box<T> local value: T fn init(value: T) self.value = value endend
local b: Box<integer> = Box("no") -- ERROR: Box<string> is not Box<integer>class Box<T> local value: T fn init(value: T) self.value = value endend
local c: Box<integer, string> = Box(1) -- ERROR: `Box` expects 1 type argumentNaming the class without its arguments means “some instantiation, unknown which”, and is accepted against any of them:
class Box<T> local value: T
fn init(value: T) self.value = value end
fn get() -> T return self.value endend
local any: Box = Box(1) -- oklocal back: Box<integer> = any -- okprintln(back.get()) -- 1Like a function’s, a class’s type parameters are erased at runtime — they constrain the program, they don’t reach it.