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 writtenArgumentInfers
value: T7i32T = int32
value: &T, &var Ta local counter: CounterT = Counter — borrowed
p: *T@counterT = Counter
values: T[..]an int32[3] array or a sliceT = int32
pair: Pair<T>a Pair<int64>T = int64
f: func(T) -> Ua function func(int32) -> boolT = int32, U = bool
outcome: T ! Ean int32 ! FaultT = 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.

Typed arms over a generic sum.
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

DeclarationGeneric?Page
functionyesthis page
structure, variantyesGeneric types
methodyes — its own parameters, as well as its type'sGeneric methods
enumno — enum 'Level' cannot declare type parametersEnums
union, type aliasno
interfaceno — see Interfaces

See also