Generic bound
An unconstrained <T> promises nothing about T. The body may store a T, pass it on and hand it back, but it cannot call a method on it, because nothing says the method exists. A bound makes that promise. <T: Scored> accepts only types that implement the interface Scored, and in return the body may call Scored's methods on any T. It is the generic counterpart of the Interface lesson: the same interface, used to choose which types may come in.
A bound is written after a colon
Here is the interface, and one type that implements it:
interface Scored {
func Score() -> int32;
}
extend Player : Scored {
func Score(self: &Player) -> int32 {
return self.points;
}
}
Best names the interface after its type parameter:
func Best<T: Scored>(first: T, second: T) -> T {
if second.Score() > first.Score() {
return second;
}
return first;
}
Score is callable on first and second only because of the bound. Remove : Scored and the body itself is rejected — not a particular call, the definition — because a plain T has no Score.
The winner comes back as a T. Called with two Player values, Best returns a Player, so winner.name and winner.points are right there; it never turns into "some Scored".
The check happens at the call
flowchart LR
call["Best(ana, bo)"] --> t["T = Player"]
t --> q{"Does Player<br/>implement Scored?"}
q -- "yes" --> inst["Best is compiled for Player;<br/>Score() calls Player's Score"]
q -- "no" --> err["error at the call:<br/>type argument … does not satisfy<br/>interface bound 'Scored'"]The compiler works out T from the arguments first, then checks the bound. A type without an implementation is rejected where it is passed in, with a note naming the method it is missing — never somewhere deep inside the body. That makes the bound a contract you can read from the signature alone: anything Scored may be passed, and nothing else.
Primitives qualify only through an implementation
int32 has no Score until something gives it one. An extend block on a primitive does, and from then on a bare number is its own score:
extend int32 : Scored {
func Score(self: &int32) -> int32 {
return self;
}
}
Note the call in Main:
PrintLine("best number {}", Best<int32>(3, 9));
The <int32> matters. Two unsuffixed literals make T an int — the Literal rule — and int is a different type from int32, one that nothing has made Scored. Writing T out turns 3 and 9 into int32 values.
A bound travels
Margin has a bound of its own, and that is what lets it call Best:
func Margin<T: Scored>(first: T, second: T) -> int32 {
let winner = Best(first, second);
return winner.Score() * 2 - first.Score() - second.Score();
}
Inside Margin, T is known only to be Scored. That is exactly what Best requires, so the call is accepted. A generic that calls another generic must promise at least what the callee demands.
A bound costs nothing at run time
A bound and an interface value both use an interface, but they work differently:
Bound: <T: Scored> | Interface value: Scored | |
|---|---|---|
| Which type? | one concrete type per call, fixed when compiling | any implementing type, decided while running |
| Method call | direct, as if written for that type | looked up through the value at run time |
| What comes back | the same T that went in — a Player | a Scored; the Player is no longer visible |
| Mixed types in one call | no — first and second are both T | yes |
Each instantiation calls its own type's method directly, as if Best had been written out by hand for Player and again for int32.
The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// An unconstrained `<T>` promises nothing about `T`, so a generic body can only move a `T`
// around. A bound changes that: `<T: Scored>` accepts only types that implement `Scored`, and
// in return the body may call `Scored`'s methods on any `T`.
//
// The check happens at the call. A type without an implementation is rejected where it is
// passed in, with a note naming the method it is missing, never somewhere deep in the body.
//
// Unlike an interface value, a bound costs nothing at run time. Each instantiation calls its
// own type's method directly, as if the function had been written out by hand for that type.
import Io::PrintLine;
interface Scored {
func Score() -> int32;
}
struct Player {
name: char8[..];
points: int32;
}
extend Player : Scored {
func Score(self: &Player) -> int32 {
return self.points;
}
}
// Primitives satisfy a bound the same way: only through an implementation. `int32` has no
// `Score` until this block gives it one, and then a bare number is its own score.
extend int32 : Scored {
func Score(self: &int32) -> int32 {
return self;
}
}
// `Score` is callable on `first` and `second` only because of the bound. The winner is handed
// back as a `T`, so a `Player` comes back as a `Player`, not as a `Scored`.
func Best<T: Scored>(first: T, second: T) -> T {
if second.Score() > first.Score() {
return second;
}
return first;
}
// How far the winner leads. A bound travels: a `T` that is `Scored` here satisfies `Best` too.
func Margin<T: Scored>(first: T, second: T) -> int32 {
let winner = Best(first, second);
return winner.Score() * 2 - first.Score() - second.Score();
}
func Main() -> int {
let ana = Player { name: "Ana", points: 12 };
let bo = Player { name: "Bo", points: 17 };
let winner = Best(ana, bo);
PrintLine("best player {} with {}", winner.name, winner.points);
PrintLine("margin {}", Margin(ana, bo));
// An unsuffixed literal is an `int`, which has no `Score`, so the type is written out.
PrintLine("best number {}", Best<int32>(3, 9));
// `Best(2.5, 1.5)` is rejected: "type argument 'float64' does not satisfy interface bound
// 'Scored'". A `float64` can be compared with `>`, but nothing has made it `Scored`.
return 0;
}
Run it
cd Examples/Generics/GenericBound
rux run
best player Bo with 17
margin 5
best number 9
Common mistakes
T.Without the bound,
second.Score() fails with error: no interface bound on type parameter 'T' provides method 'Score', and the compiler suggests the fix: help: add a bound whose interface declares 'Score', as in 'T: SomeInterface'.Scored.Best(2.5, 1.5) fails with error: type argument 'float64' does not satisfy interface bound 'Scored' on type parameter 'T', and a note: interface 'Scored' requires method 'Score', which type 'float64' does not implement. A float64 can be compared with >, but nothing has made it Scored.int, not int32.Best(3, 9) fails the same way, naming 'int': the literals have no typed partner, so T is int, and only int32 was given a Score. Write Best<int32>(3, 9), or pass int32 variables.If
Margin were <T> without the bound, its call Best(first, second) would fail with error: type argument 'T' does not satisfy interface bound 'Scored' on type parameter 'T', and the help says what to do: add the bound to the enclosing declaration, as in 'T: Scored'.Try it yourself
- Add a third player and write
BestOfThree<T: Scored>(a: T, b: T, c: T) -> TusingBesttwice. - Implement
Scoredforfloat64— perhaps the value rounded down withas int32— and makeBest(2.5, 1.5)compile. - Declare a
struct Teamwith awinsfield, implementScoredfor it, and pass two teams toMargin. Nothing inMarginorBestchanges. - Try
Best(ana, 9). Why is aPlayerand anint32rejected, even though both areScored?
Learn more
- Generic functions and Interfaces in the Rux Reference
- Interface and Extension — declaring and implementing
Scored - Interface value — the run-time alternative to a bound
- Multiple bounds — requiring two interfaces at once