Interface 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 Gauge | Implementation's receiver | Callable 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" --> callerEach 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:
| Written | What the code gets | Can it change the original? |
|---|---|---|
let photo: Shape = balloon; | a copy | no — it has its own copy |
gauge: &Gauge | a read-only borrow | no |
gauge: &var Gauge | a writable borrow | yes |
The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// 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
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.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.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
- Add a
Fanwith aspeedthat keeps theGaugepromise, and whoseAdjustnever lets the speed drop below 0. Turn it down by 5 from 3. - Write
func Reset(gauge: &var Gauge)that brings any gauge back to 0 using onlyReadandAdjust. - Declare
musicwithlet. Which calls inMainstill compile, and why?
Learn more
- Mutable reference —
&varfor concrete types - Mutating method —
self: &var T - Display — a standard interface whose method takes a
&var TextWriterparameter - Interfaces in the Rux Reference