Classes
A class is a reference type with fields and methods.
class User { static created: int = 0 name: string age: int = 0 // visible inside this package pub email: string = "" // visible in every package priv token: string // visible only inside User
fn init(name: string, token: string) { self.name = name self.token = token User.created += 1 }
fn greet() -> string { return self.private_greeting() }
priv fn private_greeting() -> string { return "hi {self.name}" }
static fn guest() -> User { return new User("guest", "") }}
let u: User = new User("jul", "secret")- Methods are instance methods by default. Their
selfis implicit and available in the body; it is never written in the parameter list. static fndeclares a class method. It has noselfand is not inherited.- A normal class is constructed with
new Class(...). It always runsinit. Class field literals and plainClass(...)calls are errors. - An unmarked field is visible in its package.
pubopens it to every package.privlimits it to the class that declares it.
Anything that produces an object belongs on that object’s class, as new or as
a named static (for fallible construction, like File.open). A module-level
function is only for work that yields no object.
Two more field forms round out the model:
- Function-typed fields are callable through member syntax. A field
on_click: fn() -> intis invoked aswidget.on_click()— no local copy needed. When a class has a method and a fn-typed field with the same name, the method wins; the local-copy form still reaches the shadowed field. weakfields hold a zeroing, non-owning reference to another object — the tool that keeps parent/child graphs cycle-free. See Memory and ownership.
Field visibility
Section titled “Field visibility”Beans has three field visibility levels:
| Form | Can access the field |
|---|---|
value: int | code in the same package |
pub value: int | code in any package |
priv value: int | only the class or struct that declares it |
priv stays strict even in the same package. A peer class, subclass, or free
function cannot read or write the field. There is no protected level.
Method visibility
Section titled “Method visibility”Methods use the same three levels:
| Form | Can call the method |
|---|---|
fn read() | code in the same package |
pub fn read() | code in any package |
priv fn read() | only the class or struct that declares it |
priv works on instance, static, and inout struct methods. A private method
is not inherited and cannot be abstract or override. A subclass may declare
a new method with the same name, but it does not replace the parent’s private
method.
Static fields
Section titled “Static fields”A static field belongs to the class, not to each object. Read and write it through the class name:
class Request { static next_id: int = 1 priv static secret: int = 40
priv static fn secret_value() -> int { return Request.secret }
static fn take_id() -> int { let id: int = Request.next_id Request.next_id += 1 return id }
static fn reveal() -> int { return Request.secret_value() }}Static fields are initialized once, in declaration order, before main runs.
They need an initializer and are not inherited. A generic class cannot declare
a static field; each type argument would otherwise make ownership unclear.
Singleton classes
Section titled “Singleton classes”singleton class creates one eager instance. Access it as Type.instance:
singleton class Registry { priv count: int = 0
fn next() -> int { self.count += 1 return self.count }}
let first: int = Registry.instance.next()let second: int = Registry.instance.next()The instance is created once before main, after static fields are initialized.
Every .instance access returns that same object. new Registry() is an error.
A singleton must have a zero-argument initializer and cannot declare deinit.
It also cannot be generic, abstract, unique, or extended.
init: the constructor
Section titled “init: the constructor”init is the constructor body. new Class(...) allocates the object and runs
it. init has an implicit self, but it is a lifecycle method rather than a
normal callable method.
class Conn { host: string hits: int = 0
fn init(host: string) { self.host = host }}
let c: Conn = new Conn("db1")Callers cannot write c.init(...). Construct an object with new; the compiler
runs init exactly once. The only direct initializer call is super.init(...)
inside a subclass initializer.
- A class whose fields all have defaults gets an implicit zero-argument
initializer. A class with any required field must declare
init. - Until every field is assigned, the
initbody is a straight-line prefix: each statement either assigns a field or reads a field already assigned. No method calls, no passingselfon, noreturn, and no string interpolation. The checker rejects uses that would expose a partially initialized object. After all fields are assigned, normal statements and method calls are allowed. - A plain
fn initis package-private. Any file in the same package can writenew Conn(...). - Use
pub fn initonly when another package must construct the class. The class itself must bepubtoo. - Use
priv fn initwhen callers must go through a static factory. Even a peer class or subclass in the same package cannot call that constructor. - Construction that can fail stays a named static returning a
Result, such asstatic fn open(...) -> Result<Conn>, which may callnew Conn(...)after validation.
deinit: the destructor
Section titled “deinit: the destructor”deinit runs exactly once, on whichever thread drops the last reference, the
moment the count hits zero, and before the fields are released, so the body can
still read them. Destruction is deterministic: it happens at that point, not at
some later garbage-collector pause.
class Conn { host: string pub fn init(host: string) { self.host = host } fn deinit() { io.println("closing {self.host}") }}- No parameters, no return value, and callers cannot write
c.deinit(). The compiler runs it automatically. - A subclass
deinitruns first, then its parent’s, automatically, with nooverride. selfmust not escape adeinit.- Unreachable reference cycles run their
deinitmethods during collection. Collection timing is not scope-exit cleanup. Declare the back edge as aweakfield for release at the last strong reference (see Memory and ownership). - An object whose
inithas not returned does not run itsdeinit. If the initializer panics partway, the fields it did assign are still released, in the ordinary order, but the body is skipped — so it is never handed aselfwhose fields the initializer never reached. This covers a panic in theinitbody, in a field’s default expression, and in a baseinitreached throughsuper.init, and it applies to adeinita class inherits as much as one it declares.
That last rule applies to the object under construction. Previously initialized
fields are released normally. Once every field is assigned, the initializer may
pass out a reference that keeps the object alive. In that case, deinit runs
when the object’s lifetime ends.
If you relied on deinit running after a failed construction, to close a handle
or unregister something the initializer had already taken, move that work out of
the destructor. Acquire the resource through a named static returning
Result<T> that validates before it calls new, so a failure never builds the
object at all.
Partial classes
Section titled “Partial classes”partial class Name splits one class across files in the same package. Every
part says partial; exactly one primary part carries modifiers, generic
parameters, extends, and implements. Other parts use only
partial class Name { ... }. All fields and methods belong to one class, and a
duplicate member is still an error.
pub partial class Shape extends Figure { name: string}
// shape_text.bpartial class Shape { fn describe() -> string { return self.name }}The primary is decided by its header, not file load order. partial remains an
ordinary name anywhere except directly before class.
Inheritance and interfaces
Section titled “Inheritance and interfaces”Classes take one base class with extends and implement interfaces with
implements. Construction chains through super.init(...). That is covered in
Interfaces and inheritance.
Compiler-owned builtin types such as Bytes, File, and List cannot be base
classes. The compiler rejects class X extends Bytes at that declaration; it
does not continue and report a missing parent constructor later.
Self: fluent chains that keep their type
Section titled “Self: fluent chains that keep their type”An instance method of a class or interface may declare -> Self. At every
call site the result has the receiver’s own static type, so a chain inherited
from a base class does not degrade to the base mid-chain:
package main
import std.io
class Base { value: int = 0 pub fn tune(n: int) -> Self { self.value = self.value + n return self }}
class Special extends Base { pub fn only_here() -> int { return self.value * 100 }}
fn main() { // tune() comes from Base but returns Special here io.println("{new Special().tune(1).tune(2).only_here()}") // 300}The guarantee is enforced in the body: a Self-returning method must
return self (or a chain of Self-returning calls on self). Overrides and
interface conformances match Self only against Self, a generic class’s
Self carries its own type parameters, and nothing about layout or ABI
changes. Self is not available on static methods or free functions.
A complete example
Section titled “A complete example”import std.io
class Account { owner: string balance: decimal = 0.0
fn init(owner: string) { self.owner = owner }
fn deposit(amount: decimal) { self.balance = self.balance + amount }
fn summary() -> string { return "{self.owner}: {self.balance}" }
static fn empty(owner: string) -> Account { return new Account(owner) }}
fn main() { let a: Account = Account.empty("jul") a.deposit(19.99) io.println(a.summary())}