Generic type
The Generic lesson gave a function a type parameter, so one body served int, float64 and char. A type can take a type parameter too. struct Pair<T> is not one struct but a pattern for many: Pair<int32> and Pair<char8[..]> are two different types stamped out of the same declaration. This is how a single definition can describe "two of something" or "a reading of something" without deciding in advance what that something is.
A struct with a type parameter
The parameter goes in angle brackets after the name, and the fields use it like any other type:
// Two values of the same type, whatever that type is.
struct Pair<T> {
first: T;
second: T;
}
Both fields are the same T, so a pair is always two values of one type. Each use of Pair picks its own T, and the compiler lays out a separate struct for each one — a pair of int32 holds two numbers, a pair of char8[..] holds two slices.
A type may take several parameters, and they need not agree:
// A type may take several parameters, and they need not agree.
struct Entry<K, V> {
key: K;
value: V;
}
| Written | What it is |
|---|---|
Pair<T> | the declaration — a pattern, not yet a type |
Pair<int32> | a type: two int32 fields |
Pair<char8[..]> | another type: two text fields |
Entry<char8[..], int32> | a type with two parameters, filled in order |
Making a value: the literal names its type arguments
A struct literal spells out the type arguments after the name:
let point = Pair<int32> { first: 3, second: 4 };
let words = Pair<char8[..]> { first: "left", second: "right" };
The literal does not guess T from its fields. Pair { first: 3, second: 4 } is refused, even though both fields are plainly numbers — and with <int32> written down, 3 and 4 become int32 values, just as a literal beside a typed value does.
A generic function over a generic type
A generic function can take a generic type. Here T is never written at the call: it is read out of the Pair<int32> passed in, the same way Generic inferred it from plain arguments.
func Swapped<T>(pair: Pair<T>) -> Pair<T> {
return Pair<T> { first: pair.second, second: pair.first };
}
Swapped(point) is Swapped<int32>, and it returns a Pair<int32> with the fields the other way round — 4 3.
A generic variant
Variants take type parameters the same way. A case's payload can be a T:
// A measurement that may be exact, a range, or missing altogether.
variant Reading<T> {
Exact(T),
Between(T, T),
Missing
}
A case with a payload learns T from it, the way a function learns from its arguments. A case without one has nothing to learn from, so the type has to come from somewhere else — an annotation on the variable:
let temperature = Reading::Between(18.5, 21.0);
let floor: Reading<int32> = Reading::Exact(7);
let lost: Reading<int32> = Reading::Missing;
flowchart LR
c["Reading::…"] --> q{"Does the case<br/>carry a payload?"}
q -- "yes: Between(18.5, 21.0)" --> p["T comes from the payload:<br/>Reading<float64>"]
q -- "no: Missing" --> a{"Is there an annotation<br/>or a parameter type?"}
a -- "yes" --> ok["T comes from it:<br/>Reading<int32>"]
a -- "no" --> err["error: requires 1 type argument"]An annotation also wins over the payload: floor is a Reading<int32>, so its 7 is an int32 rather than an int.
Lowest then works for every reading at once. Its match is the one from Variant match, with T standing in for the payload type:
func Lowest<T>(reading: Reading<T>, fallback: T) -> T {
return match reading {
.Exact(value) => value,
.Between(low, _) => low,
.Missing => fallback
};
}
The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// The Generic lesson gave a function a type parameter. A type can have one too: `struct Pair<T>`
// is not one struct but a pattern for many, and `Pair<int32>` and `Pair<char8[..]>` are two
// different types stamped out of it. Each one is laid out for its own `T`, so a pair of bytes is
// small and a pair of strings is wide.
//
// Variants work the same way. A generic variant's cases can carry a `T`, and that is how one
// declaration describes "a reading of something" without saying what is being read.
import Io::PrintLine;
// Two values of the same type, whatever that type is.
struct Pair<T> {
first: T;
second: T;
}
// A type may take several parameters, and they need not agree.
struct Entry<K, V> {
key: K;
value: V;
}
// A measurement that may be exact, a range, or missing altogether.
variant Reading<T> {
Exact(T),
Between(T, T),
Missing
}
// A generic function can take a generic type. `T` is inferred from the pair it is given.
func Swapped<T>(pair: Pair<T>) -> Pair<T> {
return Pair<T> { first: pair.second, second: pair.first };
}
func Lowest<T>(reading: Reading<T>, fallback: T) -> T {
return match reading {
.Exact(value) => value,
.Between(low, _) => low,
.Missing => fallback
};
}
func Main() -> int {
// A struct literal names its type arguments. A literal does not guess `T` from its fields,
// so `Pair { first: 3, second: 4 }` is rejected: "struct initializer for 'Pair' requires 1
// type argument, but 0 were provided".
let point = Pair<int32> { first: 3, second: 4 };
let words = Pair<char8[..]> { first: "left", second: "right" };
let flipped = Swapped(point);
PrintLine("point {} {}", flipped.first, flipped.second);
PrintLine("words {} {}", words.first, words.second);
let age = Entry<char8[..], int32> { key: "age", value: 42 };
PrintLine("entry {} = {}", age.key, age.value);
// A case with a payload learns `T` from it, the way a generic function learns from its
// arguments, so this is a `Reading<float64>`. An annotation supplies `T` too: `floor` is a
// `Reading<int32>`, so its 7 is an `int32`. A case without a payload has nothing to learn
// from, so the annotation is what tells `Missing` which reading it is.
let temperature = Reading::Between(18.5, 21.0);
let floor: Reading<int32> = Reading::Exact(7);
let lost: Reading<int32> = Reading::Missing;
PrintLine("lowest {}", Lowest(temperature, 0.0));
PrintLine("lowest {}", Lowest(floor, 0));
PrintLine("lowest {}", Lowest(lost, -1));
return 0;
}
Run it
cd Examples/Generics/GenericType
rux run
point 4 3
words left right
entry age = 42
lowest 18.5
lowest 7
lowest -1
Common mistakes
Pair { first: 3, second: 4 } fails with error: struct initializer for 'Pair' requires 1 type argument, but 0 were provided. Write Pair<int32> { … }. The same goes for a type with two parameters: Entry<int32> { … } is refused with requires 2 type arguments, but 1 was provided.let lost = Reading::Missing; fails with error: variant case 'Reading::Missing' requires 1 type argument, but 0 were provided. Annotate the variable — let lost: Reading<int32> = Reading::Missing; — or name the type at the case: Reading::Missing<int32>().T.Once
T is fixed, every field declared as T must be that type. Pair<int32> { first: 3, second: "four" } fails with error: field 'second' in initializer for 'Pair' has type 'char8[..]', but its declaration requires 'int32'.Try it yourself
- Make a
Pair<bool>and pass it toSwapped. You do not need to changeSwappedat all. - Write
Highest<T>(reading: Reading<T>, fallback: T) -> T, which returns the upper end of aBetween. - Add a case
Approximately(T)toReading. Which function stops compiling, and why?
Learn more
- Generic functions and Variants with data in the Rux Reference
- Generic — type parameters on functions
- Generic method — giving
Pair<T>methods of its own - Generic sum — another way to build a type out of type parameters
Overview
Types and functions that work for many types: six lessons on generic structs and variants, their methods, interface bounds, and helpers that work for every optional, fallible or sum.
13.2 Generic method
Give a generic type methods with extend Labeled<T>, including a method with a type parameter of its own.