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
endA 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
// ...
endSaying 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())
endA 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"])) // pearEach 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) intGiving 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 = []
endStandard 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)) // 6An 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.0An 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)].