Interfaces · Lesson 12.3

Interface parameter

Source
Pass any implementing type to a function through a borrowed &I or &var I parameter.

The Interface value lesson stored a copy of a shape. Often you want something else: a function that works on the caller's own value, whatever its type, as long as it keeps a promise. A parameter written &Gauge or &var Gauge does exactly that. It borrows, as &T did in Reference, and it accepts any type that implements Gauge.

A function for any gauge

// Only reads, so a shared borrow is enough.
func Show(gauge: &Gauge) {
    PrintLine("the {} reads {}", gauge.Label(), gauge.Read());
}

Show never learns whether it was handed a thermostat or a volume knob. It calls Label and Read, and dynamic dispatch runs the right type's code. The caller passes its own variable, with no & and no conversion — the conversion to the interface happens at the call:

Show(heating);
Show(music);

Nothing is copied: Show sees the caller's value as it is right now.

Requirements that write

Turning a gauge up changes it, and the interface has to say so:

interface Gauge {
    func Label() -> char8[..];
    func Read() -> int32;
    func Adjust(self: &var Self, amount: int32);
}

A requirement with no self only reads the value. One that changes it declares self: &var Self, where Self stands for whichever type implements the interface. Each implementation then takes &var of its own type, as in Mutating method:

// A volume knob stops at 10, however far it is turned.
func Adjust(self: &var Volume, amount: int32) {
    self.level += amount;
    if self.level > 10 {
        self.level = 10;
    }
}
Requirement in GaugeImplementation's receiverCallable through
func Read() -> int32;self: &Thermostat&Gauge and &var Gauge
func Adjust(self: &var Self, amount: int32);self: &var Thermostat&var Gauge only

Changes reach the caller

To call Adjust, the parameter must be a writable borrow:

func TurnUp(gauge: &var Gauge, amount: int32) {
    gauge.Adjust(amount);
}
flowchart LR
    caller["var heating<br/>(a Thermostat)"] -- "TurnUp(heating, 5)<br/>borrowed as &var Gauge" --> call["gauge.Adjust(5)"]
    call -- "dispatch" --> impl["Thermostat's Adjust"]
    impl -- "writes through the borrow" --> caller

Each type's own Adjust runs: the thermostat goes from 19 to 24, while the knob, turned from 7 by 5, stops at 10. And the last line, heating.degrees is now 24, proves the change landed on the caller's variable itself, not on a copy.

Side by side with the previous lesson:

WrittenWhat the code getsCan it change the original?
let photo: Shape = balloon;a copyno — it has its own copy
gauge: &Gaugea read-only borrowno
gauge: &var Gaugea writable borrowyes

The program

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

Src/Main.rux
// A function can ask for an interface instead of a concrete type, and then any type that keeps
// the promise may be passed to it. The caller hands over its own thermostat or volume knob, and
// the conversion to the interface happens at the call.
//
// Written with `&`, the parameter borrows, exactly as `&T` did in the Reference lesson: nothing is
// copied, and the function sees the caller's value as it is right now. Written with `&var`, the
// borrow is writable, so a change made through the interface lands on the caller's own variable.
//
// The interface has to say which of its requirements write. A requirement with no `self` only
// reads the value; one that changes it declares `self: &var Self`, where `Self` stands for
// whichever type implements the interface. Leave that out and every `Adjust` below is refused:
// "method 'Adjust' of 'Thermostat' writes through its receiver, but requirement 'Adjust' of
// interface 'Gauge' only reads it".
import Io::PrintLine;

interface Gauge {
    func Label() -> char8[..];
    func Read() -> int32;
    func Adjust(self: &var Self, amount: int32);
}

struct Thermostat {
    degrees: int32;
}

struct Volume {
    level: int32;
}

extend Thermostat : Gauge {
    func Label(self: &Thermostat) -> char8[..] {
        return "thermostat";
    }

    func Read(self: &Thermostat) -> int32 {
        return self.degrees;
    }

    // Adjusting changes the value, so this method takes `&var` self.
    func Adjust(self: &var Thermostat, amount: int32) {
        self.degrees += amount;
    }
}

extend Volume : Gauge {
    func Label(self: &Volume) -> char8[..] {
        return "volume";
    }

    func Read(self: &Volume) -> int32 {
        return self.level;
    }

    // A volume knob stops at 10, however far it is turned.
    func Adjust(self: &var Volume, amount: int32) {
        self.level += amount;
        if self.level > 10 {
            self.level = 10;
        }
    }
}

// Only reads, so a shared borrow is enough.
func Show(gauge: &Gauge) {
    PrintLine("the {} reads {}", gauge.Label(), gauge.Read());
}

// Calls `Adjust`, which changes the value, so the borrow must be writable too. Written as
// `gauge: &Gauge`, the call below is refused: "cannot call 'Adjust' through immutable reference
// '&Gauge'".
func TurnUp(gauge: &var Gauge, amount: int32) {
    gauge.Adjust(amount);
}

func Main() -> int {
    var heating = Thermostat { degrees: 19 };
    var music = Volume { level: 7 };
    Show(heating);
    Show(music);

    // Each type's own `Adjust` runs: the knob stops at 10, the thermostat does not.
    TurnUp(heating, 5);
    TurnUp(music, 5);

    // The changes were made to these very variables, not to copies of them.
    Show(heating);
    Show(music);
    PrintLine("heating.degrees is now {}", heating.degrees);

    // A writable borrow needs a writable variable. Declare `heating` with `let` and the call is
    // refused: "argument 1 to 'TurnUp' cannot borrow immutable 'heating' as '&var Gauge'".
    return 0;
}

Run it

cd Examples/Interfaces/InterfaceParameter
rux run
the thermostat reads 19
the volume reads 7
the thermostat reads 24
the volume reads 10
heating.degrees is now 24

Common mistakes

A requirement that does not say it writes.
Declare Adjust in the interface as func Adjust(amount: int32); and every implementation is refused: error: method 'Adjust' of 'Thermostat' writes through its receiver, but requirement 'Adjust' of interface 'Gauge' only reads it. Add self: &var Self to the requirement.
Calling a writing method through a read-only borrow.
With gauge: &Gauge, the call gauge.Adjust(amount) fails with error: cannot call 'Adjust' through immutable reference '&Gauge'. A function that changes the value needs &var Gauge.
Passing a let variable to a &var parameter.
A writable borrow needs a writable variable. Declare heating with let and TurnUp(heating, 5) fails with error: argument 1 to 'TurnUp' cannot borrow immutable 'heating' as '&var Gauge'. Show(heating) still works, because it only reads.

Try it yourself

  1. Add a Fan with a speed that keeps the Gauge promise, and whose Adjust never lets the speed drop below 0. Turn it down by 5 from 3.
  2. Write func Reset(gauge: &var Gauge) that brings any gauge back to 0 using only Read and Adjust.
  3. Declare music with let. Which calls in Main still compile, and why?

Learn more