Chapter 10: Interfaces
An interface names a set of methods. A class or struct that declares implements the interface and provides those methods can be used wherever the interface is expected. This chapter covers declaring and implementing interfaces, default methods, the built-in interfaces that the language itself uses (Iterable, Iterator, Comparable, Scoped, Sendable, Finalizable), and interfaces as constraints on generic code.
Declaring and implementing an interface
An interface lists method signatures, each ending in ;. A type opts in with implements and defines every method:
@mode(host.native)
interface Shape {
func area(): float;
func name(): string;
}
class Circle implements Shape {
pub let radius: float;
new(radius: float) { this.radius = radius; }
pub func area(): float { return 3.14159 * this.radius * this.radius; }
pub func name(): string { return "circle"; }
}
struct Rectangle implements Shape {
width: float;
height: float;
pub func area(): float { return this.width * this.height; }
pub func name(): string { return "rectangle"; }
}
func totalArea(shapes: list[Shape]): float {
var sum = 0.0;
for (s in shapes) {
sum = sum + s.area();
}
return sum;
}
func main(): void {
let shapes: list[Shape] = [Circle(1.0), Rectangle(width = 2.0, height = 3.0)];
for (s in shapes) {
println(i"{$ s.name() $}: {$ s.area() $}");
}
println(totalArea(shapes));
}
Listing 10-1: A class and a struct behind one interface
circle: 3.14159
rectangle: 6
9.14159
A call through an interface is dispatched on the value's actual type at run time, as with class inheritance (chapter 9). A class combines a base class and interfaces as class Dog : Animal implements Named, Comparable[Dog].
Mistake: relying on structural typing
Go and TypeScript treat any type with the right methods as satisfying an interface. Loup's interfaces are nominal: a type must say implements:
@mode(host.native)
interface Shape {
func area(): float;
}
class Square {
pub let side: float;
new(side: float) { this.side = side; }
pub func area(): float { return this.side * this.side; }
}
func main(): void {
let s: Shape = Square(2.0);
println(s.area());
}
main.lphc:14:5: error[E0001]: cannot assign 'Square' to binding 's' of type 'Shape'
Add implements Shape to the class.
Mistake: an incomplete implementation
Declaring implements is a promise to provide every method, with the declared types:
@mode(host.native)
interface Shape {
func area(): float;
func name(): string;
}
class Square implements Shape {
pub let side: float;
new(side: float) { this.side = side; }
pub func area(): float { return this.side * this.side; }
}
func main(): void {
let s: Shape = Square(2.0);
println(s.area());
}
main.lphc:8:1: error[E0001]: class 'Square' does not implement all methods of interface 'Shape'; missing: name(): string
The message lists each missing method with its signature. A method with the right name but the wrong return type is reported too: "method 'area' in class 'Square' has return type 'int', incompatible with interface 'Shape' return type 'float'".
Known gap. Stage0 1.25.1 performs this check for classes only. A struct … implements Shape that leaves out a method compiles, and fails only if the missing method is called. Rely on the class check, and test struct implementations.
Default methods
An interface method can have a body. Implementing types inherit it and may override it:
interface Shape {
func area(): float;
func name(): string;
func describe(): string {
return i"{$ this.name() $} with area {$ this.area() $}";
}
}
class Circle implements Shape {
pub let radius: float;
new(radius: float) { this.radius = radius; }
pub func area(): float { return 3.0 * this.radius * this.radius; }
pub func name(): string { return "circle"; }
}
func main(): void {
let c: Shape = Circle(2.0);
println(c.describe());
}
Listing 10-2: A default method
circle with area 12
Known gap. A default method called on a struct stops loup run with internal interpreter limitation: unsupported instruction: CallVirtual describe on user class … — method not found. Until that is fixed, give structs their own implementation of every method.
Interfaces the language uses
Iterable[T] and Iterator[T]
A for (x in value) loop works on any value whose type implements Iterable[T], which has one method, iter(), returning an Iterator[T]. An iterator's next() returns Some(item) until it returns None:
class PageIter implements Iterator[int] {
var page: int;
let last: int;
new(first: int, last: int) {
this.page = first;
this.last = last;
}
pub modifier func next(): Option[int] {
if (this.page > this.last) {
return None;
}
let current = this.page;
this.page = this.page + 1;
return Some(current);
}
}
class PageRange implements Iterable[int] {
let first: int;
let last: int;
new(first: int, last: int) {
this.first = first;
this.last = last;
}
pub func iter(): Iterator[int] {
return PageIter(this.first, this.last);
}
}
func main(): void {
for (page in PageRange(3, 5)) {
println(i"fetching page {$ page $}");
}
}
Listing 10-3: A custom iterable
fetching page 3
fetching page 4
fetching page 5
next() changes the iterator, so it is a modifier. A gen func (chapter 4) is a shorter way to produce a sequence.
Comparable[T]
Comparable[T] has one method, compareTo(other: T): int, returning a negative number, zero, or a positive number. Implementing it gives a type <, <=, >, >= and ==. int, float and string implement it already:
class Version implements Comparable[Version] {
pub let major: int;
pub let minor: int;
new(major: int, minor: int) {
this.major = major;
this.minor = minor;
}
pub func compareTo(other: Version): int {
if (this.major != other.major) {
return this.major - other.major;
}
return this.minor - other.minor;
}
}
func main(): void {
let installed = Version(1, 9);
let available = Version(2, 0);
if (installed < available) {
println(i"upgrade to {$ available.major $}.{$ available.minor $}");
}
}
Listing 10-4: Ordering versions
upgrade to 2.0
Scoped[T]
A type that implements Scoped[T] (methods scope_enter(): T and scope_exit(error: Option[Exception]): bool) can be used in a with block, which calls both on every path out of the block. Listing 5-8 is a complete example.
Finalizable
A class with a func finalize(): void method implements Finalizable implicitly; writing implements Finalizable is allowed and documents the intent. Chapter 9 explains when finalize() runs.
Sendable
Sendable has no methods. It marks a type whose values may be handed to another thread (runtime volume §2-1.8). Primitives, strings and structs of Sendable fields qualify automatically. A class must declare it, and the compiler then checks that every var field is protected by a Mutex[T], RwLock[T] or Atomic[T]:
@mode(host.native)
class RequestCounter implements Sendable {
var count: int = 0;
}
func main(): void {
let c = RequestCounter();
}
main.lphc:4:5: error[E_SENDABLE_UNPROTECTED_VAR]: var field 'count' on Sendable class 'RequestCounter' must be of type Mutex[T], RwLock[T], or Atomic[T], not 'int'
A let field is never written after construction, so it needs no protection. A synchronized class (chapter 15) protects its fields with its own lock and may declare Sendable with plain var fields. Chapter 20 covers threads.
Interfaces as generic constraints
A generic function can require its type parameter to implement an interface, and then call the interface's methods on it. Write the bound in a where clause after the return type:
func largest[T](items: list[T]): T where T: Comparable[T] {
var best = items[0];
for (x in items) {
if (x > best) {
best = x;
}
}
return best;
}
func label[S](shape: S): string where S: Shape {
return i"{$ shape.name() $} ({$ shape.area() $})";
}
func main(): void {
println(largest([3, 9, 2]));
println(largest(["pear", "apple", "zucchini"]));
println(label(Rectangle(width = 2.0, height = 2.5)));
}
Listing 10-5: where bounds
9
zucchini
rectangle (5)
Unlike a list[Shape] parameter, a bounded type parameter keeps the concrete type: largest returns an int for a list of int. The inline form of the bound, func largest[T: Comparable[T]](…), is also specified, but in 1.25.1 it does not let the body call the interface's methods; chapter 11 shows the error.
Chapter 18 covers interfaces with lifetime parameters.
Summary
interface I { func m(): T; }declares methods;class C implements Iandstruct S implements Iopt in. Interfaces are nominal (cannot assign 'Square' to … 'Shape'withoutimplements).- A class that omits an interface method is rejected; a struct is not checked in 1.25.1.
- Interface methods may have default bodies; calling one on a struct fails under
loup runtoday. Iterable[T]/Iterator[T]driveforloops;Comparable[T](compareToreturningint) gives<,>,==;Scoped[T]driveswith;finalize()makes a classFinalizable.Sendableclasses must protect everyvarfield (E_SENDABLE_UNPROTECTED_VAR).- Use
where T: Ito constrain a generic and callI's methods onT. - Chapter 11 covers generics.