Generic method
A generic type gets methods the same way any type does: through extend. The difference is one pair of angle brackets. The block is written extend Labeled<T>, and that T is in scope for every method inside, so a single block serves Labeled<int32>, Labeled<char8[..]> and every other instantiation. A method can also bring a type parameter of its own, chosen afresh at each call.
One extend block for every T
Labeled<T> is a value with a name attached:
struct Labeled<T> {
label: char8[..];
value: T;
}
Its methods live in extend Labeled<T>. Inside, T means "whatever this instantiation's T is", and the receivers are written with it — &Labeled<T> to read, &var Labeled<T> to change, exactly as in Mutating method:
extend Labeled<T> {
// A constructor, as for any struct. It returns the instantiation it was called on.
func Labeled(label: char8[..], value: T) -> Labeled<T> {
return Labeled<T> { label: label, value: value };
}
func Get(self: &Labeled<T>) -> T {
return self.value;
}
func Set(self: &var Labeled<T>, value: T) {
self.value = value;
}
To call the constructor, put the type argument on the type name, and the rest is an ordinary call:
var count = Labeled<int32>("count", 7);
count.Set(15);
From then on count is a Labeled<int32>, so Get returns an int32 and Set accepts only an int32. Nothing is written per type: the compiler produces Get for int32 because count needs it.
A method with a type parameter of its own
With<U> declares a second parameter, U, which has nothing to do with the type's T. T comes from the receiver; U comes from this call's argument:
// `T` comes from the receiver; `U` comes from this call's argument.
func With<U>(self: &Labeled<T>, other: U) -> (T, U) {
return (self.value, other);
}
The same count can be paired with text on one line and with a bool on the next. U is inferred from the argument, or written out after the method name when you want to be explicit:
let fruit = count.With("apples");
let flag = count.With<bool>(true);
| Parameter | Declared on | Fixed when | In count.With("apples") |
|---|---|---|---|
T | the type, Labeled<T> | the value is made | int32 |
U | the method, With<U> | each call, from its argument | char8[..] |
Changing the instantiation
A method's result can be a different instantiation of the same type. Map keeps the label and turns the value into something else through a callback:
func Map<U>(self: &Labeled<T>, change: func(T) -> U) -> Labeled<U> {
return Labeled<U> { label: self.label, value: change(self.value) };
}
Here U is inferred from the callback's result type. Half returns a float64 and IsLarge a bool, so the same Labeled<int32> becomes a Labeled<float64> in one call and a Labeled<bool> in the other:
let half = count.Map(Half);
let large = count.Map(IsLarge);
flowchart LR
c["count<br/>Labeled<int32>"] -- "Map(Half)<br/>Half: int32 → float64" --> h["half<br/>Labeled<float64>"]
c -- "Map(IsLarge)<br/>IsLarge: int32 → bool" --> l["large<br/>Labeled<bool>"]The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// A generic type gets methods the same way any type does, through `extend`. The block is
// written `extend Labeled<T>`, and that `T` is in scope for every method inside it: one block
// serves `Labeled<int32>`, `Labeled<char8[..]>` and every other instantiation.
//
// A method may also declare a type parameter of its own, which is chosen at each call and has
// nothing to do with the type's. `With<U>` below pairs the stored `T` with a `U` that can be
// different every time it is called.
import Io::PrintLine;
// A value with a name attached to it.
struct Labeled<T> {
label: char8[..];
value: T;
}
extend Labeled<T> {
// A constructor, as for any struct. It returns the instantiation it was called on.
func Labeled(label: char8[..], value: T) -> Labeled<T> {
return Labeled<T> { label: label, value: value };
}
func Get(self: &Labeled<T>) -> T {
return self.value;
}
func Set(self: &var Labeled<T>, value: T) {
self.value = value;
}
// `T` comes from the receiver; `U` comes from this call's argument.
func With<U>(self: &Labeled<T>, other: U) -> (T, U) {
return (self.value, other);
}
// The result can be a different instantiation of the same type: a `Labeled<T>` becomes a
// `Labeled<U>`, keeping its label and changing its value through `change`.
func Map<U>(self: &Labeled<T>, change: func(T) -> U) -> Labeled<U> {
return Labeled<U> { label: self.label, value: change(self.value) };
}
}
func Half(value: int32) -> float64 {
return (value as float64) / 2.0;
}
func IsLarge(value: int32) -> bool {
return value > 100;
}
func Main() -> int {
// The type argument goes on the type name, then the constructor runs as usual.
var count = Labeled<int32>("count", 7);
count.Set(15);
PrintLine("{} = {}", count.label, count.Get());
// `U` is inferred from the argument, or written out after the method name.
let fruit = count.With("apples");
let flag = count.With<bool>(true);
PrintLine("with {} {}", fruit.0, fruit.1);
PrintLine("with {} {}", flag.0, flag.1);
// `U` inferred from the callback's result type.
let half = count.Map(Half);
let large = count.Map(IsLarge);
PrintLine("{} / 2 = {}", half.label, half.Get());
PrintLine("{} > 100 is {}", large.label, large.Get());
return 0;
}
Run it
cd Examples/Generics/GenericMethod
rux run
count = 15
with 15 apples
with 15 true
count / 2 = 7.5
count > 100 is false
Common mistakes
extend Labeled without its parameter.extend Labeled { … } fails with error: struct type 'Labeled' requires 1 type argument, but 0 were provided, followed by error: type 'T' is not defined in this scope for every method that mentions T. The block must declare the parameter it uses: extend Labeled<T>.Labeled("count", 7) fails with error: constructor for 'Labeled' requires 1 type argument, but 0 were provided. Write Labeled<int32>("count", 7).let binding.Set takes &var Labeled<T>, so it needs a var. With let count = …, count.Set(15) fails with error: cannot call 'Set' on immutable 'count' — generics change nothing about mutability.Try it yourself
- Make a
Labeled<char8[..]>called"name"holding"Ada", and print it throughGet. - Add
func Relabel(self: &Labeled<T>, label: char8[..]) -> Labeled<T>, which returns a copy with a new label. - Write a function
Describe(value: int32) -> char8[..]that returns"small"or"large", and pass it toMap. What type is the result? - Call
count.With(count). What areTandU, and what does the tuple hold?
Learn more
- Methods in the Rux Reference
- Generic type — declaring
Labeled<T>in the first place - Extension and Constructor — the same
extendand constructors on ordinary types - Generic bound — methods a
Tis promised to have
13.1 Generic type
Declare a struct and a variant that take a type parameter — Pair<T>, Reading<T> — and use them at several types.
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.