Generics
A generic declaration takes type parameters: names in angle brackets that stand for types the user supplies. A generic function is written once and works for every type it is used with; a generic type is a pattern from which Pair<int32>, Pair<char8[..]> and so on are stamped out. Functions, methods, structures and variants can be generic.
Nothing is decided at run time. The compiler produces a separate instantiation for each distinct set of type arguments a program uses, as if each had been written by hand, with its own layout, its own code and its own symbol.
Generic functions
func Name<T, U, …>(parameters) -> Result { … }
func Name<T: Bound + Bound, U>(parameters) -> Result { … }
func Larger<T>(first: T, second: T) -> T {
if first > second {
return first;
}
return second;
}
Each type parameter is in scope in the parameter list, the result type and the body. A parameter may carry bounds, interfaces its type arguments must implement.
Type arguments
A call supplies the type arguments explicitly, after the name, or leaves the compiler to infer them from the arguments:
PrintLine("{}", Larger(3, 9)); // T = int, inferred
PrintLine("{}", Larger(2.5, 1.5)); // T = float64
PrintLine("{}", Larger<uint8>(200, 7) + 100); // T = uint8: prints 44
An explicit type argument also types the arguments: with <uint8>, the literals 200 and 7 become uint8 values, and so does the result, which is why adding 100 wraps. Without it, unsuffixed literals make T an int.
Inference reads type arguments out of the argument types, wherever the parameter mentions them:
| Parameter written | Argument | Infers |
|---|---|---|
value: T | 7i32 | T = int32 |
value: &T, &var T | a local counter: Counter | T = Counter — borrowed |
p: *T | @counter | T = Counter |
values: T[..] | an int32[3] array or a slice | T = int32 |
pair: Pair<T> | a Pair<int64> | T = int64 |
f: func(T) -> U | a function func(int32) -> bool | T = int32, U = bool |
outcome: T ! E | an int32 ! Fault | T = int32, E = Fault |
value: T? | an int32? | T = int32 |
A type parameter that no argument determines must be written: Size<int16>(). A sum of type parameters is not inferred either, since nothing says which member is T: Side(v) with func Side<T, U>(value: T | U) is refused, and Side<int32, bool>(v) is the call. Inference does not look at the result type or the destination, so let f: int32? = Make(); with func Make<T>() -> T? is function 'Make' requires 1 type argument, but 0 were provided. Inference never relaxes a parameter's other requirements: a &var T parameter still needs a writable argument.
Two arguments that disagree about one parameter are refused — Larger(3, 2.5):
error: argument 1 to 'Larger' has type 'int', but parameter 'first' requires 'T'
Checking a generic body
A generic body is checked in two stages.
At the declaration, everything that does not depend on what T is: names, statements, and every use of T that needs a capability. A method call on a T, or passing a T where an interface is required, needs a bound that provides it:
error: argument 2 to 'PrintLine' has type 'T', but variadic parameter 'args' requires 'Display'
At each instantiation, the operators applied to T. first > second in Larger is checked once T is known, so Larger(3, 9) and Larger('a', 'z') compile and Larger("abc", "abd") does not. The error points into the generic body, and a note names the call responsible:
error: operator '>' is not defined for slice type 'char8[..]'
note: a slice is a view, so comparing the views would compare addresses rather than elements
note: in 'Larger' instantiated with T = char8[..] by the call at …
Without a bound, a body may store a T, pass it on, return it, copy or move it, take its size with sizeof(T), and apply operators that every instantiation will have to support.
Generic functions are not values
A function value has one concrete signature, and a generic function has one per instantiation, so a generic function cannot be stored in a function-typed variable or passed as a callback: let f: func(int32) -> int32 = Identity; fails with cannot assign 'func(T) -> T' to 'func(int32) -> int32'. Wrap the instantiation in an ordinary function instead. A generic function may freely take a function value whose type mentions its parameters, as Apply<T>(value: T, f: func(T) -> T) does.
Overloading
Generic functions overload like any other, by arity and by parameter types. When a generic and a non-generic overload accept a call equally well, the non-generic one wins:
func Describe<T>(value: T) -> char8[..] {
return "generic";
}
func Describe(value: int32) -> char8[..] {
return "int32";
}
Describe(5i32) is int32; Describe(true) is generic. See Overloading for the full ranking.
Native forms
Optionals, sums and fallibles are built into the language and have no declaring package, so extend cannot give them methods. Generic functions whose parameters spell the form out take their place, and work for every payload and error type at once:
func ValueOr<T, E>(outcome: T ! E, fallback: T) -> T {
return outcome catch { else => fallback };
}
func Both<T, U>(first: T?, second: U?) -> (T, U)? {
return (first?, second?);
}
Core::Succeeded<T, E>(outcome: T ! E) -> bool and Core::Failed are written this way.
A sum of type parameters collapses when its members coincide: T | U with T = U = int32 is plain int32. A match over a T | U therefore ends in else, which stays valid for every instantiation, where a second typed arm would be unreachable for the collapsed one.
rux 0.4.0 does not yet lower a typed arm that selects a concrete type, such as
n: int32 =>, from a sum of type parameters; the build fails with cannot lower the selection of 'int32' from 'T | U'. Match on the type parameters themselves, first: T =>, with an else arm.Kinds of generic declaration
| Declaration | Generic? | Page |
|---|---|---|
| function | yes | this page |
| structure, variant | yes | Generic types |
| method | yes — its own parameters, as well as its type's | Generic methods |
| enum | no — enum 'Level' cannot declare type parameters | Enums |
| union, type alias | no | |
| interface | no — see Interfaces |
See also
- Generic types — structures and variants with type parameters
- Generic methods — methods of generic types, and methods with their own type parameters
- Bounds — what a type argument must implement
- Function types — callbacks in generic signatures
- Learn: Generic, Generics, Generic outcome, Generic sum
Iteration
for walks arrays, slices and ranges directly, and any other type through Next(self: &var T) -> Item? or an Iterate method that hands out an iterator.
Generic Types
Structures and variants take type parameters. Every use names its type arguments, and each instantiation is a distinct type with its own layout.