Chapter 9: Interfaces and Generics
What you'll learn: how to declare an interface and implement it, give an interface default methods, use the standard interfaces that power ==, <, for and with, overload operators, and write generic functions and classes — including how to constrain a type parameter with where.
Two features let one piece of code work with many types. An interface names a set of methods; any class that implements them can be used where the interface is expected. Generics let a function or class take a type as a parameter, so Stack[int] and Stack[string] share one implementation and are both fully type-checked.
Part 1: Interfaces
Declaring and implementing an interface
@mode(host.scripted)
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.14159 * this.radius * this.radius;
}
pub func name(): string {
return "circle";
}
}
class Rectangle implements Shape {
pub let width: float;
pub let height: float;
new(width: float, height: float) {
this.width = width;
this.height = height;
}
pub func area(): float {
return this.width * this.height;
}
pub func name(): string {
return "rectangle";
}
}
func totalArea(shapes: list[Shape]): float {
var total = 0.0;
for (shape in shapes) {
total = total + shape.area();
}
return total;
}
let shapes: list[Shape] = [Circle(1.0), Rectangle(3.0, 4.5)];
for (shape in shapes) {
println(shape.describe());
}
println(i"total: {$ totalArea(shapes) $}");
Listing 9-1: An interface with two required methods and one default method
circle with area 3.14159
rectangle with area 13.5
total: 16.64159
interface Shape { … }lists method signatures. A signature ending in;is required; a method with a body is a default that implementers get for free (§1-11.1).class Circle implements Shapepromises to provide every required method. Interface methods are public, so the implementations arepub.list[Shape]holds any mix of implementers, andtotalAreaworks for shapes that do not exist yet. Nothing aboutCircleandRectangleis related except the interface.- Inside a default method,
thisis the implementing object; call the other methods asthis.area().
A class can extend one class and implement several interfaces: class Thumbnail : Image implements Shape, Closeable { … }. Writing an interface where the superclass goes, class Circle : Shape, means the same as class Circle implements Shape (§1-8.3).
Mistake: leaving out a required method
@mode(host.scripted)
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;
}
}
main.lpscr:8:1: error[E0001]: class 'Square' does not implement all methods of interface 'Shape'; missing: name(): string
The message lists every missing method with its signature. (The Language Reference calls this error E_INTERFACE_NOT_IMPLEMENTED; Loup 1.25 reports it as E0001.) Add pub func name(): string { return "square"; }.
Mistake: a signature that does not match
An implementation must use the interface's parameter and return types exactly:
@mode(host.scripted)
interface Shape {
func area(): float;
}
class Square implements Shape {
pub let side: int;
new(side: int) {
this.side = side;
}
pub func area(): int {
return this.side * this.side;
}
}
main.lpscr:7:1: error[E0001]: method 'area' in class 'Square' has return type 'int', incompatible with interface 'Shape' return type 'float'
Return float, converting the integer: return (this.side * this.side) as float;.
Overriding a default method
An implementing class may replace a default method by declaring its own:
interface Greeter {
func name(): string;
func greet(): string {
return i"Hello, {$ this.name() $}!";
}
}
class Friendly implements Greeter {
pub func name(): string {
return "Ada";
}
}
class Formal implements Greeter {
pub func name(): string {
return "Dr. Lovelace";
}
pub func greet(): string {
return i"Good afternoon, {$ this.name() $}.";
}
}
let greeters: list[Greeter] = [Friendly(), Formal()];
for (g in greeters) {
println(g.greet());
}
Hello, Ada!
Good afternoon, Dr. Lovelace.
The Language Reference requires
overrideon a method that replaces an interface default (§1-8.3.1). The 1.25 compiler rejects it there instead —pub override func greet()inFormalreportsE_OVERRIDE_NO_TARGETbecause "classFormalhas no supertype" — so leaveoverrideoff when replacing an interface default. (Replacing a method inherited from a class does needoverride; see Chapter 8.)
Interfaces that extend interfaces
An interface can build on others with :. An implementer must provide the methods of the whole chain:
interface Named {
func name(): string;
}
interface Priced : Named {
func cents(): int;
func label(): string {
return i"{$ this.name() $}: ${$ this.cents() // 100 $}.{$ this.cents() % 100 $}";
}
}
class Item implements Priced {
pub let title: string;
pub let price: int;
pub func name(): string {
return this.title;
}
pub func cents(): int {
return this.price;
}
}
let widget = Item(title = "Widget", price = 1250);
println(widget.label());
let asNamed: Named = widget;
println(asNamed.name());
println(widget is Named);
Widget: $12.50
Widget
true
x is I tests whether a value implements an interface at run time, and x as I converts it once you know.
Standard Interfaces
The language and standard library define interfaces that hook into syntax (§1-11.2):
| Interface | Methods you write | What it enables |
|---|---|---|
Equatable[T] |
operator func ==(other: T): bool |
== and != (Chapter 8) |
Comparable[T] |
compareTo(other: T): int |
<, >, <=, >= |
Hashable |
hashCode(): int |
use as a dict key or set element (see the gap below) |
Iterable[T], Iterator[T] |
iter(): Iterator[T]; next(): Option[T] |
for (x in value) |
Closeable |
close(): void |
with (x = value) { … } (Chapter 8) |
Finalizable |
finalize(): void |
cleanup on del (Chapter 8) |
Dynamic |
provided by dynamic class |
obj!{field} (Chapter 8) |
Add, Sub, Mul, Div, … |
operator func +(…) and friends |
operator overloading (below) |
From[E] |
static func from(err: E): Self |
error conversion with ? (Chapter 10) |
Comparable: ordering
compareTo returns a negative number, zero or a positive number for "less than", "equal" and "greater than":
class Version implements Comparable[Version] {
pub let major: int;
pub let minor: int;
pub let patch: int;
pub func compareTo(other: Version): int {
if (this.major != other.major) {
return this.major - other.major;
}
if (this.minor != other.minor) {
return this.minor - other.minor;
}
return this.patch - other.patch;
}
pub operator func ==(other: Version): bool {
return this.compareTo(other) == 0;
}
pub func text(): string {
return i"{$ this.major $}.{$ this.minor $}.{$ this.patch $}";
}
}
let installed = Version(major = 1, minor = 4, patch = 2);
let required = Version(major = 1, minor = 10, patch = 0);
println(installed < required);
println(installed == Version(major = 1, minor = 4, patch = 2));
var releases = [required, installed, Version(major = 0, minor = 9, patch = 9)];
releases.sort(func(a: Version, b: Version) => a.compareTo(b));
for (v in releases) {
println(v.text());
}
Listing 9-2: A comparable version number
true
true
0.9.9
1.4.2
1.10.0
Notice that 1.4.2 < 1.10.0, which a string comparison would get wrong. list.sort always takes a comparator function; passing a.compareTo(b) sorts in the type's natural order.
Why does Version also define ==? The Language Reference says Comparable[T] extends Equatable[T] and supplies == from compareTo (§1-11.2). Loup 1.25 does not do that yet:
class Version implements Comparable[Version] {
pub let major: int;
pub let minor: int;
pub func compareTo(other: Version): int {
if (this.major != other.major) {
return this.major - other.major;
}
return this.minor - other.minor;
}
}
let installed = Version(major = 1, minor = 4);
let required = Version(major = 1, minor = 4);
println(installed >= required);
println(installed == required);
true
false
>= uses compareTo, but == still compares identity. Until the default arrives, give every Comparable class an explicit operator func ==, as Listing 9-2 does.
Iterable and Iterator: making a type work with for
A type works with for when it implements Iterable[T], whose iter() returns a fresh Iterator[T]. The iterator's next() returns Some(value) until it returns None:
class CountdownIterator implements Iterator[int] {
var current: int;
new(start: int) {
this.current = start;
}
pub modifier func next(): Option[int] {
if (this.current <= 0) {
return None;
}
let value = this.current;
this.current = this.current - 1;
return Some(value);
}
}
class Countdown implements Iterable[int] {
pub let start: int;
new(start: int) {
this.start = start;
}
pub func iter(): Iterator[int] {
return CountdownIterator(this.start);
}
}
for (n in Countdown(3)) {
println(n);
}
println("liftoff");
Listing 9-3: A custom iterable
3
2
1
liftoff
next() changes the iterator's current field, so it is a modifier (Chapter 8); implementing a non-modifier interface method with a modifier one is allowed. Because iter() creates a new iterator each time, a Countdown can be looped over more than once. For most cases a generator — gen func (Chapter 3) — is shorter.
Hashable
Hashable declares hashCode(): int. Two values that are == must have the same hash code.
Known gap. In Loup 1.25,
dictkeys andsetelements that are class instances are compared by identity, even when the class implementsEquatableandHashable: twoTag("a")objects are two different keys. Use strings, numbers, enums or structs as keys until this is implemented.
⚠ Planned.
Renderable(func render(ctx: RenderContext): string) is specified to control how a value appears in interpolated strings and template fences (§1-11.2, Chapter 12). In 1.25 the typeRenderContextis not defined and interpolation does not callrender; an object interpolates asClassName(field: value, …).
Operator Overloading
Each operator belongs to an interface: + to Add, - to Sub, * to Mul, / to Div, % to Mod, == to Equatable, a[k] to Index, and so on (§1-5.1). To overload an operator, implement the interface and write operator func:
class Vector2 implements Add[Vector2, Vector2], Sub[Vector2, Vector2], Mul[float, Vector2] {
pub let x: float;
pub let y: float;
new(x: float, y: float) {
this.x = x;
this.y = y;
}
pub operator func +(other: Vector2): Vector2 {
return Vector2(this.x + other.x, this.y + other.y);
}
pub operator func -(other: Vector2): Vector2 {
return Vector2(this.x - other.x, this.y - other.y);
}
pub operator func *(factor: float): Vector2 {
return Vector2(this.x * factor, this.y * factor);
}
}
let a = Vector2(1.5, 2.0);
let b = Vector2(3.0, 4.5);
println(a + b);
println(b - a);
println(a * 2.0);
var position = Vector2(0.0, 0.0);
position += a;
position += a;
println(position);
Listing 9-4: Vector arithmetic with operator interfaces
Vector2(x: 4.5, y: 6.5)
Vector2(x: 1.5, y: 2.5)
Vector2(x: 3.0, y: 4.0)
Vector2(x: 3.0, y: 4.0)
The two type arguments are the right-hand operand's type and the result type: Mul[float, Vector2] means "Vector2 * float gives a Vector2". position += a is rewritten to position = position + a, so it works as soon as + does.
Mistake: forgetting the interface
@mode(host.scripted)
class Vector2 {
pub let x: float;
pub let y: float;
new(x: float, y: float) {
this.x = x;
this.y = y;
}
pub operator func +(other: Vector2): Vector2 {
return Vector2(this.x + other.x, this.y + other.y);
}
}
let sum = Vector2(1.0, 2.0) + Vector2(3.0, 4.0);
main.lpscr:12:5: error[E_OPERATOR_NO_INTERFACE]: E_OPERATOR_NO_INTERFACE: operator func '+' on 'Vector2' requires `implements Add` on the enclosing type
main.lpscr:17:11: error[E_OPERATOR_MISSING]: type 'Vector2' does not implement 'Add' required for operator '+' (E_OPERATOR_MISSING)
The first error is at the declaration, the second at the use. Add implements Add[Vector2, Vector2] to the class header.
Mistake: one type argument
The Language Reference declares interface Add[T, R = Self], so implements Add[Vector2] should mean Add[Vector2, Vector2]. Loup 1.25 does not support the default yet:
@mode(host.scripted)
class Meters implements Add[Meters] {
pub let value: float;
pub operator func +(other: Meters): Meters {
return Meters(value = this.value + other.value);
}
}
main.lpscr:3:25: error[E0001]: type 'Add' expects 2 type argument(s), got 1
main.lpscr:6:5: error[E_OPERATOR_NO_INTERFACE]: E_OPERATOR_NO_INTERFACE: operator func '+' on 'Meters' requires `implements Add` on the enclosing type
Write both arguments: implements Add[Meters, Meters].
Some operators cannot be overloaded at all: and, or, not, ??, ?, =, ., ?., is, as, as?, typeof and instanceof.
⚠ Planned. Unary minus (
interface Neg { operator func -(): Self; }) is specified but not implemented in 1.25: anoperator func -()with no parameter is rejected, and a class cannot declare both a unary and a binary-.
Interfaces or Inheritance?
| Use a base class when… | Use an interface when… |
|---|---|
the types really are kinds of one thing (a Cat is an Animal) |
unrelated types share a capability (a file and a socket can both be Closeable) |
| the subclasses share fields and implementation | a type has several independent capabilities |
| the hierarchy is small and stable | other people should be able to plug in their own implementations |
A class has at most one superclass but any number of interfaces, and in Loup 1.25 interfaces are also the more complete feature: the compiler checks that every required method is present, which it does not yet do for abstract classes (Chapter 8).
Part 2: Generics
Generic functions
A type parameter goes in square brackets after the name. The compiler infers it from the arguments:
func firstOr[T](items: list[T], fallback: T): T {
if (items.length() == 0) {
return fallback;
}
return items[0];
}
func swapEnds[T](items: list[T]): list[T] {
if (items.length() < 2) {
return items;
}
var result = items.clone();
let first = result[0];
result[0] = result[-1];
result[-1] = first;
return result;
}
println(firstOr([3, 4], 0) + 1);
let noNames: list[string] = [];
println(firstOr(noNames, "nobody"));
println(swapEnds([1, 2, 3, 4]));
println(swapEnds(["a", "b", "c"]));
println(firstOr[int]([7], 0));
Listing 9-5: Generic functions with inferred type arguments
4
nobody
[4, 2, 3, 1]
["c", "b", "a"]
7
firstOr([3, 4], 0) + 1 type-checks because T is int at that call, so the result is an int. firstOr[int](…) gives the type argument explicitly. Note the typed noNames: an empty literal [] has no element type of its own, so firstOr([], "nobody") fails to infer T; give the empty list a type first.
Generic classes
class Stack[T] {
var items: list[T] = [];
pub modifier func push(item: T): void {
this.items.push(item);
}
pub modifier func pop(): Option[T] {
if (this.items.length() == 0) {
return None;
}
return Some(this.items.pop());
}
pub func peek(): Option[T] {
if (this.items.length() == 0) {
return None;
}
return Some(this.items[-1]);
}
pub func size(): int {
return this.items.length();
}
}
let undo: Stack[string] = Stack();
safe {
undo.push("type 'hello'");
undo.push("delete word");
}
var last: Option[string] = None;
safe {
last = undo.pop();
}
println(last.unwrap_or("nothing to undo"));
println(undo.peek().unwrap_or("empty"));
println(undo.size());
Listing 9-6: A generic stack
delete word
type 'hello'
1
T stands for whatever the stack holds. A Stack[string] only accepts strings: undo.push(42) is a compile error.
When the constructor takes a T, the type argument is inferred from the arguments, as for functions: with class Box[T] { pub let value: T; new(value: T) { this.value = value; } }, Box(41) is a Box[int].
Mistake: Stack[int]() without a type annotation
Stack has no constructor parameter to infer T from, so you might pass the type argument in the call. In Loup 1.25 that is not enough:
@mode(host.scripted)
class Stack[T] {
var items: list[T] = [];
pub modifier func push(item: T): void {
this.items.push(item);
}
}
let numbers = Stack[int]();
safe {
numbers.push(1);
}
main.lpscr:13:18: error[E0001]: argument 1 of 'push': expected 'T0', found 'int'
T0 is the compiler's name for the unresolved type parameter: the explicit [int] on the constructor call is not applied to the new object. Annotate the binding instead, as Listing 9-6 does: let numbers: Stack[int] = Stack();.
Generic structs
Structs and enums take type parameters the same way (Chapters 6 and 7):
struct Pair[A, B] {
first: A;
second: B;
}
func swap[A, B](pair: Pair[A, B]): Pair[B, A] {
return Pair(first = pair.second, second = pair.first);
}
let setting = Pair(first = "port", second = 8080);
let flipped = swap(setting);
println(flipped.first + 1);
println(flipped);
8081
Pair(first: 8080, second: "port")
Constraining a type parameter with where
Inside a generic function, T could be any type, so you can only do with a T what every type supports. Comparing two Ts is not one of those things:
@mode(host.scripted)
func largest[T](items: list[T]): T {
var best = items[0];
for (item in items) {
if (item > best) {
best = item;
}
}
return best;
}
main.lpscr:6:13: error[E0001]: type 'T0' has no method 'compareTo'
> is defined through Comparable.compareTo, and nothing says T has it. Add a constraint with a where clause after the return type (§1-11.5):
func largest[T](items: list[T]): T where T: Comparable[T] {
var best = items[0];
for (item in items) {
if (item > best) {
best = item;
}
}
return best;
}
class Version implements Comparable[Version] {
pub let major: int;
pub let minor: int;
pub func compareTo(other: Version): int {
if (this.major != other.major) {
return this.major - other.major;
}
return this.minor - other.minor;
}
}
println(largest([3, 9, 4]));
println(largest(["pear", "apple", "plum"]));
let newest = largest([Version(major = 1, minor = 2), Version(major = 1, minor = 10)]);
println(i"{$ newest.major $}.{$ newest.minor $}");
Listing 9-7: A constrained generic function
9
plum
1.10
Built-in types implement the standard interfaces: int, float and string are all Comparable. Several constraints are separated by commas: where T: Comparable[T], T: Hashable.
Known gaps in generic constraints (Loup 1.25).
- The Language Reference also allows the constraint inside the brackets,
func largest[T: Comparable[T]](…), and joining bounds with+(T: Comparable[T] + Hashable, §1-12). The bracket form is accepted but not used — the body still reportstype 'T0' has no method 'compareTo'— and+in awhereclause does not parse. Usewherewith commas. The same applies to class type parameters:class SortedBag[T: Comparable[T]]cannot compare its elements yet, and awhereclause on a method does not help either.- A constraint is not checked where the function is called. The Language Reference makes a call with a type that does not satisfy it a compile error (
E_WHERE_BOUND_NOT_MET); in 1.25 the call compiles and fails when the missing method is reached, as shown below.- A value of a type parameter cannot be passed where
Anyis expected:str(value),println(value)ori"{$ value $}"withvalue: Treportexpected 'Any', found 'T0'. Return the value to code that knows its type, or format it there.
What an unmet constraint does today:
func largest[T](items: list[T]): T where T: Comparable[T] {
var best = items[0];
for (item in items) {
if (item > best) {
best = item;
}
}
return best;
}
class Crate {
pub let kilos: int;
}
let heaviest = largest([Crate(kilos = 10), Crate(kilos = 30)]);
println(heaviest.kilos);
Uncaught RuntimeError: internal interpreter limitation: unsupported instruction: CallVirtual compareTo on user class Crate — method not found
Crate does not implement Comparable, so there is no compareTo to call. Implement Comparable[Crate] (comparing kilos) and the call works.
A Complete Example: A Small Data Pipeline
Interfaces and generics together give you code that you extend by adding classes, not by editing it:
interface Source[T] {
func name(): string;
func rows(): list[T];
}
interface Step[T] {
func apply(rows: list[T]): list[T];
}
interface Sink[T] {
func write(rows: list[T]): int;
}
func runPipeline[T](source: Source[T], step: Step[T], sink: Sink[T]): int {
let rows = source.rows();
let kept = step.apply(rows);
return sink.write(kept);
}
class LinesSource implements Source[string] {
pub let text: string;
pub func name(): string {
return "lines";
}
pub func rows(): list[string] {
return this.text.split("\n");
}
}
class DropBlank implements Step[string] {
pub func apply(rows: list[string]): list[string] {
return rows.filter(func(row: string) => row.trim().length() > 0);
}
}
class PrintSink implements Sink[string] {
pub let prefix: string;
pub func write(rows: list[string]): int {
for (row in rows) {
println(this.prefix + row);
}
return rows.length();
}
}
let written = runPipeline(
LinesSource(text = "alpha\n\nbeta\n \ngamma"),
DropBlank(),
PrintSink(prefix = "> "),
);
println(i"{$ written $} rows written");
Listing 9-8: A pipeline of pluggable stages
> alpha
> beta
> gamma
3 rows written
runPipeline knows nothing about lines, blank rows or printing. A CSV source, a deduplicating step or a file sink are new classes that implement Source[T], Step[T] or Sink[T]; runPipeline does not change.
Summary
- An interface lists required methods (ending in
;) and default methods (with a body); a classimplementsit and must provide every required method with the exact signature. - In 1.25, replace an interface default without
override; replace a superclass method withoverride. - Interfaces can extend interfaces;
x is Iandx as Itest and convert at run time. Comparablegives<,>,<=,>=; define==explicitly as well.Iterable/Iteratormake a type work withfor;Closeableworks withwith.- Operators are overloaded by implementing their interface with both type arguments (
Add[Other, Result]) and writingoperator func. - Generic functions and classes take type parameters in square brackets; arguments are inferred where possible. Annotate the binding when a constructor has nothing to infer from.
- Constrain type parameters with
where T: Interface[T]; in 1.25 the bracket form is not used and unmet constraints fail at run time. - Class-instance keys in
dictandset,Renderableand unary-overloading are not implemented yet.
Try It Yourself
- Add a
Triangleto Listing 9-1 (Heron's formula needssqrtfromcore.math) and check thattotalAreaneeds no change. - Implement
Comparable[Version]on a version with a pre-release tag (1.4.0-betasorts before1.4.0), sort a list, and print it. - Write
class Queue[T]withmodifiermethodsenqueueanddequeue(): Option[T], and use it with strings and withVersionobjects. - Write
func countAbove[T](items: list[T], limit: T): int where T: Comparable[T]and call it with numbers and with strings. - Add a
Step[string]that upper-cases every row (toUpperCase()) and chain two steps in Listing 9-8 by writing aChain[T]step that holds two other steps.