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
- Bounds go inline (
<T implements X>).whereclauses for longer constraint lists are deferred by design designed. - Bounded generics compose with lambdas:
items.map(i => i.describe())works whereitems: Array<T>in progress.
Language_Generics.md holds the full design, including conversion interfaces such as From<T>.
Next: Capabilities