Generics & Interfaces

Lumina's generics look like TypeScript's, but the compiler monomorphizes them the way Rust does: each instantiation is compiled to specialized native code, with no boxing or runtime type erasure. Interfaces describe behavior, and you can implement them inside a struct or after the fact.

Generic functions

function lastOf<T>(items: Array<T>): T = items[items.length() - 1]

lastOf([1, 2, 3])       // 3
lastOf(["a", "b"])      // "b"

The type parameter is inferred at the call site.

Generic structs

struct Stack<T> {
    items: Array<T>

    public function push(mut self, item: T): Void {
        self.items.push(item)
    }

    public function size(self): Integer = self.items.length()
}

mut s = Stack<String> { items: [] }
s.push("x")
s.push("y")
s.size()                // 2

Interfaces

An interface lists method signatures:

interface Describable {
    function describe(self): String
}

struct Dog {
    name: String
}

implements Describable for Dog {
    public function describe(self): String = `dog {self.name}`
}

The implements Describable for Dog { ... } block attaches the interface to a type you already have. This is like a Rust impl Trait for Type, and you can use it for types you did not write. You can also declare conformance inline, as in struct FixedClock implements Clock { ... }, with the methods in the struct body. Capabilities uses that style.

In the external form, the interface's methods must be inside the implements block. If the methods sit in the struct body and the implements block is empty, the compiler reports "does not implement".

Bounded generics vs. interface types

There are two ways to accept "anything describable":

// Static dispatch: specialized per concrete type.
function announce<T implements Describable>(item: T): String = `Look: {item.describe()}`

// Dynamic dispatch: one function, the call goes through the interface.
function log(value: Describable): String = value.describe()

announce(Dog { name: "Rex" })     // "Look: dog Rex"
log(Dog { name: "Fido" })         // "dog Fido"

The bound is written <T implements Interface>. The <T: Interface> spelling from Rust is not accepted.

Operator overloading

@Operator maps an operator to a method:

struct Vec2 {
    public x: Integer
    public y: Integer

    @Operator("+")
    public function add(self, other: Vec2): Vec2 = Vec2 { x: self.x + other.x, y: self.y + other.y }
}

v = Vec2 { x: 1, y: 2 } + Vec2 { x: 10, y: 20 }    // 11,22

The whole program

interface Describable {
    function describe(self): String
}

struct Dog {
    name: String
}

implements Describable for Dog {
    public function describe(self): String = `dog {self.name}`
}

struct Vec2 {
    public x: Integer
    public y: Integer

    @Operator("+")
    public function add(self, other: Vec2): Vec2 = Vec2 { x: self.x + other.x, y: self.y + other.y }
}

struct Stack<T> {
    items: Array<T>

    public function push(mut self, item: T): Void {
        self.items.push(item)
    }

    public function size(self): Integer = self.items.length()
}

function lastOf<T>(items: Array<T>): T = items[items.length() - 1]

function announce<T implements Describable>(item: T): String = `Look: {item.describe()}`

function log(value: Describable): String = value.describe()

function main(console: Console): Integer {
    console.println(`{lastOf([1, 2, 3])} {lastOf(["a", "b"])}`)
    v = Vec2 { x: 1, y: 2 } + Vec2 { x: 10, y: 20 }
    console.println(`{v.x},{v.y}`)
    mut s = Stack<String> { items: [] }
    s.push("x")
    s.push("y")
    console.println(`size {s.size()}`)
    console.println(announce(Dog { name: "Rex" }))
    console.println(log(Dog { name: "Fido" }))
    return 0
}
3 b
11,22
size 2
Look: dog Rex
dog Fido

Bounds and lambdas

Language_Generics.md holds the full design, including conversion interfaces such as From<T>.

Next: Capabilities