Classes and inheritance
After this page you can attach methods to data with a class, extend a class
with a subclass, and bind a protocol to a record. Classes are the only values
that answer value.method() calls.
Use a record and free functions by default. Use a class when the methods belong to the value, such as a counter or a builder.
The examples on this page build records and classes, so run them with
semaprax run file.spx --native. check needs no flag.
Add methods
module example.counter;
@id("example.counter")
class Counter {
@id("example.counter.value")
value: i64,
@id("example.counter.get")
fn get(self: Counter) -> i64
{
self.value
}
@id("example.counter.bumped")
fn bumped(self: Counter, amount: i64) -> Counter
{
Counter { value: self.value + amount }
}
}
@id("app.main")
fn main() -> i64
{
let base = Counter { value: 40 };
let next = base.bumped(2);
if next.value == 42 && base.get() == 40 { next.value } else { 0 }
}
- The first parameter is
self: Counter, written out. - A method usually returns a changed copy.
basestays at 40. - A class literal names every field. A field changes with
counter.value = …on alet mutbinding. - Records have no methods:
point.get()isSPX-T203. Calling a method on a number or string is the same error: usestring_len(s).
Extend a class
module example.inheritance;
@id("example.animal")
class Animal {
@id("example.animal.legs")
legs: i64,
@id("example.animal.speak")
fn speak(self: Animal) -> i64
{
self.legs
}
}
@id("example.dog")
class Dog : Animal {
@id("example.dog.bark_count")
bark_count: i64,
@id("example.dog.speak")
fn speak(self: Dog) -> i64
{
super.speak() + self.bark_count
}
}
@id("example.main")
fn main() -> i64
{
let d = Dog { legs: 4, bark_count: 2 };
let a: Animal = d;
if d.speak() == 6 && a.speak() == 4 { d.speak() } else { 0 }
}
class Dog : Animalinherits the fields and methods ofAnimal. ADogliteral names all fields, inherited ones too.super.speak()calls the parent’s method.- A
Dogis anAnimal:let a: Animal = d;converts it, and calls throughause the parent’s methods. - A method with the same name overrides the parent’s for the subclass.
Keep the tree shallow. Every extra level adds fields to every literal. To reuse behavior without substituting types, hold the other value in a field.
Bind a protocol to a record
A protocol lists the functions a type must have. An impl binds each one to
a function you already wrote, by @id. The compiler checks that every required
function is bound exactly once. The binding is checked and then erased: it
adds no dispatch, no protocol value, and no runtime cost.
module geometry.app;
@id("geometry.read-x")
protocol ReadX {
@id("geometry.read-x.get")
fn get(self: Self) -> i64;
}
@id("geometry.point")
record Point {
@id("geometry.point.x")
x: i64,
}
@id("geometry.point.read-x")
impl "geometry.read-x" for "geometry.point" {
"geometry.read-x.get" = "geometry.point.get";
}
@id("geometry.point.get")
fn get(point: Point) -> i64
{
point.x
}
@id("app.main")
fn main() -> i64
{
get(Point { x: 7 })
}
The receiver must be a local record with an @id. Bound functions are
top-level, non-generic, and not main. Projects import protocols with
use protocol @id("…") from module as name;.
Exact rules: Class Inheritance v1, Static Protocol Conformance v1.