Interfaces · Lesson 12.1

Interface

Source
Declare an interface and make a type keep its promise with extend T : I.
You'll need: Method, Extension

Different types often answer the same question in their own way. A circle and a rectangle both have an area, but each works it out differently. An interface gives that shared question a name: it lists the functions a type must provide, and a type that implements the interface promises to provide every one of them. This lesson is about making that promise and having the compiler hold you to it. The next two lessons show what other code can do with a type once it has made one.

Declaring an interface

interface Shape {
    func Area() -> float64;
    func Name() -> char8[..];
}

An interface lists function headers — names, parameters and result types — and ends each one with ; where a body would go. It has no fields and no code of its own. It only names what must exist.

The requirements carry no self. The interface says what each implementation must be able to answer; each implementation supplies its own receiver.

Keeping the promise

A type makes the promise in an extend block, the same block you met in Extension, with : Shape after the type name:

extend Circle : Shape {
    func Area(self: &Circle) -> float64 {
        return 3.14 * self.radius * self.radius;
    }

    func Name(self: &Circle) -> char8[..] {
        return "circle";
    }
}

Inside, each requirement is written as an ordinary method, now with self: &Circle as its first parameter. Rectangle keeps the same promise with its own formula. The two halves divide the work like this:

WrittenWho writes itWhat it says
interface Shape { … }the interfacewhich functions must exist
extend Circle : Shape { … }each implementing typehow this type provides each of them

The compiler checks the promise word for word. Delete Name from the Circle block and the program is refused: error: implementation of interface 'Shape' for type 'Circle' is missing method 'Name'.

Methods that keep a promise are called like any other method:

PrintLine("a {} with area {}", wheel.Name(), wheel.Area());
PrintLine("a {} with area {}", door.Name(), door.Area());

More than the promise

An implementation may add methods beyond the promised ones:

// An implementation may add methods beyond the promised ones. This one belongs to
// Rectangle alone; Shape knows nothing about it.
func IsSquare(self: &Rectangle) -> bool {
    return self.width == self.height;
}

door.IsSquare() works because door is a Rectangle. Shape knows nothing about IsSquare, which will matter in the next lesson, where a value is known only as "some Shape".

Any type can make the promise

The implementing type does not have to be a struct you wrote. A primitive type such as int32 can implement Shape just as well:

extend int32 : Shape {
    func Area(self: &int32) -> float64 {
        return 0.0;
    }

    func Name(self: &int32) -> char8[..] {
        return "number";
    }
}

After this, let n: int32 = 4; answers n.Name() with number. An interface describes behaviour, not a family of related types: anything that can answer the questions may join.

The program

The whole lesson is one package in the Examples repository. Its comments explain every step.

Src/Main.rux
// An interface is a promise a type can make: a list of functions it will provide. The interface
// has no fields and no code of its own. It only names what must exist.
//
// A type makes the promise with `extend Type : Interface { ... }`, and the compiler holds it to
// it: leave a function out and the `extend` is rejected. Copy each signature exactly as well — a
// different parameter or result type breaks the promise, though the compiler does not catch that
// yet. That checked promise is this lesson. The next two show what other code can do with a
// type once it has made one.
//
// Any type can make the promise, including one you did not write: `extend int32 : Shape` would be
// just as valid as the two below.
import Io::PrintLine;

// The requirements. They carry no `self`: the interface says what each implementation must be
// able to answer, and each implementation supplies its own receiver.
interface Shape {
    func Area() -> float64;
    func Name() -> char8[..];
}

struct Circle {
    radius: float64;
}

struct Rectangle {
    width: float64;
    height: float64;
}

// `: Shape` after the type name is the promise. Inside, each requirement is written as an
// ordinary method, now with `self` as its first parameter.
extend Circle : Shape {
    func Area(self: &Circle) -> float64 {
        return 3.14 * self.radius * self.radius;
    }

    func Name(self: &Circle) -> char8[..] {
        return "circle";
    }
}

extend Rectangle : Shape {
    func Area(self: &Rectangle) -> float64 {
        return self.width * self.height;
    }

    func Name(self: &Rectangle) -> char8[..] {
        return "rectangle";
    }

    // An implementation may add methods beyond the promised ones. This one belongs to
    // Rectangle alone; Shape knows nothing about it.
    func IsSquare(self: &Rectangle) -> bool {
        return self.width == self.height;
    }
}

func Main() -> int {
    let wheel = Circle { radius: 2.0 };
    let door = Rectangle { width: 0.9, height: 2.0 };

    // Methods that keep a promise are called like any other method.
    PrintLine("a {} with area {}", wheel.Name(), wheel.Area());
    PrintLine("a {} with area {}", door.Name(), door.Area());
    PrintLine("is the door square? {}", door.IsSquare());

    // Delete `Name` from the Circle block and the compiler refuses the program: "implementation
    // of interface 'Shape' for type 'Circle' is missing method 'Name'".
    return 0;
}

Run it

cd Examples/Interfaces/Interface
rux run
a circle with area 12.56
a rectangle with area 1.8
is the door square? false

Common mistakes

Leaving out a requirement.
Every function the interface lists must appear in the extend block. Without Name, the compiler stops with error: implementation of interface 'Shape' for type 'Circle' is missing method 'Name'.
Changing a requirement's signature.
Copy each header from the interface exactly — the same parameters and the same result type — and add only the self receiver in front. A Circle whose Area returned float32 would not be answering the question Shape asks, even though the name matches.
Forgetting : Shape.
A plain extend Circle { … } with the right methods in it makes no promise: the methods exist, but Circle is not a Shape. Nothing complains until you use it as one — let s: Shape = wheel; then fails with error: cannot assign 'Circle' to 'Shape'.
Misspelling the interface.
extend Circle : Shap { … } is refused with error: interface 'Shap' is not defined. The interface must be declared, or imported, before a type can implement it.

Try it yourself

  1. Add struct Triangle { base: float64; height: float64; } and make it keep the Shape promise. Its area is half the base times the height.
  2. Add a third requirement, func Perimeter() -> float64;, to Shape. Read the errors, then implement it for every shape.
  3. Make a square door, Rectangle { width: 1.0, height: 1.0 }, and check that IsSquare says so.

Learn more