Interfaces

An interface names the methods a type must have. A type satisfies it by having them, with the same parameter and result types:

interface Shape do
    func area(self) f64
    func label(self, str prefix) str
end

struct Circle do
    f64 radius

    func area(self) f64 do
        return 3.0 * self.radius * self.radius
    end

    func label(self, str prefix) str do
        return prefix + "circle"
    end
end

A struct or an enum can say which interfaces it satisfies, after its name and a :, joined with + when there are several. The compiler then checks it at the declaration, and says there which method is missing or has other types, rather than where a value of the type is first used as the interface:

struct Circle: Shape do
    f64 radius
    // area and label, as above
end

struct Labelled: Shape + ToText do
    // ...
end

struct Box[T]: Container[T] do
    // ...
end

Saying it is up to the type: one that does not, but has the methods, satisfies the interface all the same, so a type can satisfy interfaces of modules it does not know.

An interface is a type. A value of any type that satisfies it can be used where it is expected, and calling one of its methods calls the method of the value's own type:

Shape[] shapes = [Circle(1.0), Square(2.0)]
for s in shapes do
    print(s.label("a "), s.area())
end

A value of an interface type is a reference: it can be null, and == says whether two of them hold the same value of the same type. s.label without parentheses is a function value, as for other methods, and methods declared on the interface itself, as in func Shape.twice(self) f64, work for every value of it. An interface can have type parameters, as in interface Container[T] do func get(self, int i) T end, and a value of Container[int] is then any type with a get that returns an int. An interface cannot be made with a constructor.

Bounds on type parameters

A generic function works for any type T, so inside it a value of type T can only be stored, passed on, and returned: nothing says that a T can be compared with > or added with +. A bound says what T must be able to do. It is an interface, written after the type parameter and a :, and inside the function the values of T can then do what that interface promises.

Ordered is one of the standard interfaces: the types that can be put in order, which numbers, characters, strings, and plain enums are, and any type with a compare method. With it, largest may compare its items with >:

func largest[T: Ordered](T[] items) T do
    T best = items[0]
    for item in items do
        if item > best then
            best = item
        end
    end
    return best
end

print(largest([3, 9, 4]))                  // 9
print(largest(["pear", "apple", "fig"]))   // pear

Each use must give T a type that satisfies the bound, and where one does not, the compiler says which method it lacks:

struct Point do
    int x
    int y
end

largest([Point(1, 2)])
// error: T is 'Point' here, which does not satisfy 'Ordered': 'Point' has
// no method func compare(self, Point other) int

Giving Point that method makes it Ordered, and largest then works for it too. A type parameter can need several interfaces, joined with +, and structs, enums, and interfaces take bounds the same way:

struct SortedList[T: Ordered + Equal] do
    T[] items = []
end

Standard interfaces

The prelude declares these interfaces. Self in them is the type that satisfies them:

Interface Method What uses it
Equal func equals(self, Self other) bool == and !=
Ordered func compare(self, Self other) int <, <=, >, >=, and so sort
Hash func hash(self) i64 hash tables
ToText func to_text(self) str print, to_str, and f-strings
Add func add(self, Self other) Self + and +=
Subtract func subtract(self, Self other) Self - and -=
Multiply func multiply(self, Self other) Self * and *=
Divide func divide(self, Self other) Self / and /=
Remainder func remainder(self, Self other) Self % and %=
Negate func negate(self) Self unary -
Reader func read(self, int count) Result[bytes] sources of bytes: up to count of them, empty at the end
Writer func write(self, bytes data) Result[int] destinations of bytes: all of data, or the error that stopped it

Numbers, characters, strings, and plain enums satisfy the first four as the language already treats them, bool all but Ordered, and tuples Equal and ToText when their items do. Numbers satisfy the arithmetic ones as their operators work, integers alone Remainder, and strings Add. A struct or enum satisfies one by having its method, and the operators then call it: a == b is a.equals(b), except that null on either side is equal only to null, a < b is a.compare(b) < 0, a + b is a.add(b), total += a is total = total.add(a), -a is a.negate(), and print(a) prints a.to_text(). Reader and Writer have no operator: they let one function read from or write to any source or destination of bytes, such as a std/buffer buffer. So a numeric type of a program's own, such as an amount of money or a big integer, is written with the operators, and a generic function bounded by Add works for it and for numbers alike:

struct Money do
    i64 cents

    func add(self, Money other) Money do
        return Money(self.cents + other.cents)
    end
end

func total[T: Add](T[] items, T start) T do
    T sum = start
    for item in items do
        sum += item
    end
    return sum
end

print(total([Money(1250), Money(250)], Money(0)).cents)   // 1500
print(total([1, 2, 3], 0))                                // 6

An operator calls a method only for two values of the same type, so price + 1 is an error unless the program converts 1 first.

struct Version do
    int major
    int minor

    func compare(self, Version other) int do
        if self.major != other.major then
            return self.major - other.major
        end
        return self.minor - other.minor
    end

    func to_text(self) str do
        return f"v{self.major}.{self.minor}"
    end
end

Version[] versions = [Version(2, 0), Version(1, 9)]
sort(versions)
print(versions[0], largest(versions))   // v1.9 v2.0

An interface that uses Self other than for self, as Equal does, can bound type parameters but cannot be the type of a value, since its methods need the type itself. ToText can, and holds values of any printable type: ToText[] things = [1, "two", Version(3, 0)].