The Loup Native Book — contents
  1. Chapter 1: Getting started
  2. Chapter 2: Native mode overview
  3. Chapter 3: Variables and numerics
  4. Chapter 4: Functions
  5. Chapter 5: Control flow
  6. Chapter 6: Collections
  7. Chapter 7: Structs
  8. Chapter 8: Enums and pattern matching
  9. Chapter 9: Classes and ARC
  10. Chapter 10: Interfaces
  11. Chapter 11: Generics
  12. Chapter 12: Error handling
  13. Chapter 13: Modules and packages
  14. Chapter 14: Template fences
  15. Chapter 15: Ownership verbs
  16. Chapter 16: borrow, excl borrow and noescape
  17. Chapter 17: unique ownership
  18. Chapter 18: Lifetimes and regions
  19. Chapter 19: @unsafe and calling C
  20. Chapter 20: Concurrency with OS threads
  21. Chapter 21: Async on native
  22. Chapter 22: Desktop UI
  23. Chapter 23: SIMD and GPU
  24. Chapter 24: Building and distributing
  25. Chapter 25: Memory safety patterns

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:

This code does not compile
@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:

This code does not compile
@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]:

This code does not compile
@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 I and struct S implements I opt in. Interfaces are nominal (cannot assign 'Square' to … 'Shape' without implements).
  • 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 run today.
  • Iterable[T]/Iterator[T] drive for loops; Comparable[T] (compareTo returning int) gives <, >, ==; Scoped[T] drives with; finalize() makes a class Finalizable.
  • Sendable classes must protect every var field (E_SENDABLE_UNPROTECTED_VAR).
  • Use where T: I to constrain a generic and call I's methods on T.
  • Chapter 11 covers generics.