Multiple bounds
One bound gives a generic one set of methods. Often it needs two: Scored to decide who wins, and Display to print the winner with {}. Bounds are joined with +. <T: Scored + Display> accepts only a type that implements both interfaces, and the body may use everything either one provides.
Joining bounds with +
func Announce<T: Scored + Display>(first: T, second: T) {
if second.Score() > first.Score() {
PrintLine("{} beats {}", second, first);
} else {
PrintLine("{} holds off {}", first, second);
}
}
Score() is allowed because of Scored. Passing first and second to {} is allowed because of Display. Inside the body a T is exactly what its bounds say it is, and nothing more — with only <T: Scored>, each PrintLine here is rejected, as the Generic lesson warned it would be.
flowchart LR
t(["A type argument T"]) --> s{"implements<br/>Scored?"}
s -- "no" --> e1["error: does not satisfy<br/>interface bound 'Scored'"]
s -- "yes" --> d{"implements<br/>Display?"}
d -- "no" --> e2["error: does not satisfy<br/>interface bound 'Display'"]
d -- "yes" --> ok["Announce compiled for T:<br/>may call Score() and print with {}"]Each type meets each bound its own way
Player needs both implementations. Scored is the one from Generic bound; Display comes from the Text package, and a player prints as their name:
// A player prints as their name.
extend Player : Display {
func WriteDisplay(self: &Player, writer: &var TextWriter, spec: FormatSpec) -> ! FormatError {
return writer.Write(self.name);
}
}
int32 is already Display — the standard packages give every number that implementation, which is why numbers have always printed with {}. So it needs only Scored:
// `int32` already implements `Display` in the standard packages, so it needs only `Scored`.
extend int32 : Scored {
func Score(self: &int32) -> int32 {
return self;
}
}
| Type | Scored | Display | Announce accepts it? |
|---|---|---|---|
Player | extend Player : Scored | extend Player : Display | yes |
int32 | extend int32 : Scored | from the standard packages | yes |
float64 | none | from the standard packages | no — not Scored |
int | none | from the standard packages | no — not Scored |
That last row is why Main declares its numbers as int32 before passing them:
let low: int32 = 3;
let high: int32 = 9;
Announce(high, low);
Every bound is checked on its own
The compiler checks each bound separately and reports the one that fails. Announce(2.5, 1.5) is rejected because float64 is Display but not Scored. Delete the extend Player : Display block and Announce(ana, bo) is rejected the other way round — Player is Scored but cannot print. There is no partial credit: a type that meets one bound out of two is as unwelcome as one that meets none.
The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// One bound gives a generic one set of methods. When it needs two, the bounds are joined with
// `+`: `<T: Scored + Display>` accepts only a type that implements both interfaces, and the
// body may use everything either one provides.
//
// Here `Scored` decides who wins and `Display` lets the body print a `T` with `{}`. With only
// `<T: Scored>`, each `PrintLine` below is rejected: "argument 2 to 'PrintLine' has type 'T',
// but variadic parameter 'args' requires 'Display'". A placeholder needs `Display`, and a
// `T` is only what its bounds say it is.
import Io::PrintLine;
import Text::{ Display, FormatError, FormatSpec, TextWriter };
interface Scored {
func Score() -> int32;
}
struct Player {
name: char8[..];
points: int32;
}
extend Player : Scored {
func Score(self: &Player) -> int32 {
return self.points;
}
}
// A player prints as their name.
extend Player : Display {
func WriteDisplay(self: &Player, writer: &var TextWriter, spec: FormatSpec) -> ! FormatError {
return writer.Write(self.name);
}
}
// `int32` already implements `Display` in the standard packages, so it needs only `Scored`.
extend int32 : Scored {
func Score(self: &int32) -> int32 {
return self;
}
}
func Announce<T: Scored + Display>(first: T, second: T) {
if second.Score() > first.Score() {
PrintLine("{} beats {}", second, first);
} else {
PrintLine("{} holds off {}", first, second);
}
}
func Main() -> int {
let ana = Player { name: "Ana", points: 12 };
let bo = Player { name: "Bo", points: 17 };
Announce(ana, bo);
let low: int32 = 3;
let high: int32 = 9;
Announce(high, low);
// Each bound is checked on its own. `Announce(2.5, 1.5)` is rejected because `float64`
// is `Display` but not `Scored`; a type that is `Scored` but cannot print is rejected too.
return 0;
}
Besides Io, its Rux.toml lists Text under [Dependencies].
Run it
cd Examples/Generics/MultipleBounds
rux run
Bo beats Ana
9 holds off 3
Common mistakes
+.<T: Scored, Display> does not mean "both". The comma starts a second type parameter, here confusingly named Display, so T is bound by Scored alone. Every PrintLine in the body then fails with error: argument 2 to 'PrintLine' has type 'T', but variadic parameter 'args' requires 'Display', and every call with error: function 'Announce' requires 2 type arguments, but 0 were provided.With only
<T: Scored>, printing a T fails with has type 'T', but variadic parameter 'args' requires 'Display'. Everything the body does with a T has to come from some bound.Without
extend Player : Display, Announce(ana, bo) fails with error: type argument 'Player' does not satisfy interface bound 'Display' on type parameter 'T', with the note interface 'Display' requires method 'WriteDisplay', which type 'Player' does not implement.Try it yourself
- Swap
anaandboin the call toAnnounce. Which line is printed now? - Make a player print as
Bo (17)by callingWriteFormatinsideWriteDisplay, asMoneydid in Display. It comes from the Format package, so addFormatto[Dependencies]too. - Add a
struct Teamthat isScoredbut notDisplay, and read the error when you pass two teams toAnnounce. Then give it aDisplayimplementation.
Learn more
- Interfaces and Generic functions in the Rux Reference
- Generic bound — one bound, and how it is checked
- Display — implementing
WriteDisplay - Format — what the
specpassed toWriteDisplaydescribes
13.3 Generic bound
Bound a type parameter by an interface — <T: Scored> — so the generic can call its methods, and see that a primitive qualifies only through an implementation.
13.5 Generic outcome
Write reusable helpers over T ! E and T? as generic functions, since optionals and fallibles cannot be extended.