Skip to content

Interfaces

Interfaces allow you to declare functions and provide guarantees that any type that has implemented the interface can be passed into the function. You can declare interfaces using the interface keyword:

interface ToStr<type T> {
fn to_str(T val) -> str
}

The type parameters required by the interface are declared inside angle brackets <>, similar to generics. To implement the interface onto a type, use the impl keyword:

struct Complex {
float real
float imag
}
impl ToStr<Complex> {
fn to_str(Complex val) -> str {
return "({val.real})+({val.imag})i"
}
}

You can call the interface’s functions just like any other function:

test "interface function call" {
str out = ToStr.to_str(Complex{.real = 3.0, .imag = 4.0})
assert out == "(3.0)+(4.0)i"
}

You can also use interfaces as type constraints in function generics by using the impl keyword after a semicolon in the type parameters:

impl ToStr<str> {
fn to_str(str val) -> str {
return val
}
}
fn join<type T; impl ToStr<T>>(T[] args) -> str {
mut str output = ""
for i in args {
output = output <> ToStr.to_str(args[i])
}
return output
}
test "interface as type constraint" {
assert join<str>(["hello", "world"]) == "helloworld"
}

Interfaces can have multiple type parameters:

interface Add<type Lhs, type Rhs, type Output> {
fn add(Lhs lhs, Rhs rhs) -> Output
}
impl Add<int, int, int> {
fn add(int lhs, int rhs) -> int {
return lhs + rhs
}
}
fn sum<type T; impl Add<T,T,T>>(T[] args) -> T {
// ...
}
test "multiple generic function with single type param" {
assert sum<int>([1, 2, 3]) == 6
}

The type signature fn sum<type T; impl Add<T,T,T>>(T[] args) -> T means that the 3 type parameters of the Add interface are all the same type, and only one type needs to be declared when calling the function. If you wanted them to be different types, you could declare them as such:

fn sum<
type Lhs, type Rhs, type Out;
impl Add<Lhs,Rhs,Out>
>(Lhs[] lhs, Rhs[] rhs) -> Out[] {
mut Out[] outputs = []
for i in lhs {
outputs = outputs <> [Add.add(lhs[i], rhs[i])]
}
return outputs
}
test "multiple generic function with multiple type params" {
assert sum<int, int, int>([1, 2, 3], [4, 5, 6]) == [5, 7, 9]
}

Anything before the ; in the generic specifies the parameters you need to specify when calling it, and the constraints are added after. You can have multiple constraints too:

interface Add<type L, type R, type O> {
fn add(L lhs, R rhs) -> O
}
fn sum<
type L, type R, type O;
impl Add<L, R, O>, impl Add<R, L, O> // enforcing commutativity
>(L[] lhs, R[] rhs) -> O { ... }

Interface constraints can be used anywhere generics can be used.