Generics.
Functions, structs, unions, interfaces and impls can take parameters: types, lifetimes and effect rows. Each use with concrete arguments produces a separate specialized instance.
Syntax#
GenericParameters = '<' GenericParameter (',' GenericParameter)* ','? '>'
GenericParameter = Identifier (':' Bound ('+' Bound)*)? (type parameter)
| Lifetime (':' Lifetime ('+' Lifetime)*)? (lifetime parameter)
| '?' Identifier (row parameter)
Bound = Type | Lifetime
TypeArguments = '<' (Type | Lifetime) (',' (Type | Lifetime))* '>'
GenericCall = Path TypeArguments? '(' Arguments? ')'
OwnerMember = Path TypeArguments '.' Identifier TypeArguments?
WhereClause = 'where' Constraint (',' Constraint)*
Constraint = Type 'in' Type
| Type 'provides' Type 'from' Type
OpaqueResult = '->' 'some' GenericParameters Type
ExactType = 'typeof' '(' Path ')'Where parameters appear#
fn name<T>(...),struct Name<T> { ... },union Name<T> { ... },interface Name<T> { ... }.impl<T> Interface for Type<T> { ... }andimpl<T> Type<T> { ... }. See Interfaces and impls.An inherent
impl<T> Type<T>must list exactly the owner's parameters, in order (SEM0194).Its parameters may take interface,
Copyand outlives bounds:impl<T: Copy + Add<T, T>> Pair<T> { fn sum(self: Self) -> T { … } }. The block's members then exist only where the bounds hold. Each call proves them, and a call for other arguments is SEM0121. A type may have one unbounded block and several bounded ones. A callable bound, such asimpl<F: fn(i32) -> i32> Runner<F>, is still SEM0194.The members of a bounded block may call the bounds' operations as a bounded generic function does: qualified, as
Zero.zero()orTwice.twice(value), or as a method, asvalue.twice().Members of an inherent impl may add their own parameters:
fn map<U>(self: Self, u: U) -> Box<U>.A parameter name may appear only once in a list (SEM0050). An interface cannot declare
Selfas a parameter, sinceSelfis already bound there (SEM0050).Using a parameter of one kind where another kind is expected is SEM0088, for example
Box<'a>for a type parameter. Leaving out a requirement-row argument, as inHolder<i32>forstruct Holder<T, ?R>, is SEM0088 too; writeHolder<i32 ? never>for an empty row.
Type arguments and inference#
Type arguments are inferred from the value arguments:
swap(Pair { first: 1, second: true }).Inference uses only the arguments. It never uses the expected result type. A parameter that occurs only in the result cannot be inferred (SEM0052).
This holds for an owner's arguments too:
let l: List<i32> = List.make()is SEM0052. WriteList<i32>.make().Explicit arguments come in
<...>after the name:identity<i32>(42).An explicit list may be a prefix; inference fills the rest:
pick<i32>(40, true).A parameter left undetermined by both the prefix and the arguments is SEM0099.
An explicit type argument may leave its lifetimes out, at a function call and at a method call alike:
first<&i32>(x, &local),r.pick<View>(a, b). Each omitted lifetime is inferred from the operands. When nothing determines it, as inOption<&i32>.Nonewith no operand, it is SEM0099 at the construction; write the lifetime. See Lifetimes.Too many type arguments is SEM0051.
An argument that contradicts an explicit type argument is reported as a type mismatch (SEM0012).
A generic type's arguments go on the type:
Box<i32> { v: 1 },Maybe<i32>.None. They may be inferred in struct literals.A member that uses its owner's parameters takes the owner's arguments on the owner:
Box<bool>.map<i32>(...), notBox.map<i32>(...)(SEM0221).
Bounds#
T: Interfacerequires a conformance ofTto the interface. Bounds combine with+:T: Add<T, T> + Order.T: 'arequiresTto stay valid for'a.T: 'staticmeansTholds no borrows.F: fn(i32) -> i32bounds a parameter by a callable contract. See Closures and callable types.T: Copyuses the sealed property fromsilk/core/ownership.Dropis not usable as a bound.A generic body is checked once, against its bounds only. Using an operation the bounds don't provide is an error even before any call (SEM0135 for an operator).
Inside the body, a type parameter conforms only through the bounds it declares. Calling a function that needs a bound the parameter lacks is SEM0121 at the call, and the message names the bound to add: "
H: main.Namedcannot be proved: a type parameter conforms only through its declared bounds; addmain.Namedto the bounds ofH".An operator on a generic type may come from a conditional impl, such as
impl<T: Copy + Multiply<T, T>> Multiply<Point<T>, Point<T>> for Scale<T>. A generic body may use that operator when its own bounds prove the impl's bounds. When they don't, it is SEM0121 at the operator.At each use, the compiler proves the bounds for the concrete arguments. A missing conformance is SEM0121.
Bounds on a generic struct are checked wherever the struct type is named with arguments (SEM0121).
Where clauses#
wherefollows the result and rows of a function. It holds membership and provider constraints. It does not holdT: Bound; write bounds inline.where S in Esays failure typeSis a member of failure unionE.where ?S in Rdoes the same for requirement rows. See Typed failures and Services and providers.where P provides X from Yrelates a provider type to the service requirements it supplies. See Services and providers.
Other parameter kinds#
Lifetime parameters:
fn first<'a, T>(items: &'a [T]) -> View<'a, T>. See Lifetimes.Row parameters:
?Rstands for an effect requirement row, as ineffect fn twice<?R>(step: Effect<'static; i32 ? R>) -> i32 ? R. See Services and providers.Static value parameters are ordinary parameters marked
static, as infn pick(static table: [i32; 3], index: usize). They are not in the<...>list. See Static evaluation.
Specialization#
Each distinct set of concrete arguments gives a separate instance, compiled separately. There is no runtime dispatch.
A conformance witness is selected per instance.
Recursion must keep the same type arguments. A recursive call that changes them, as in
expand<[T; 1]>fromexpand<T>, would need infinitely many instances and is rejected (SEM0053).
Opaque results#
-> some<F: Bound> Fhides the exact result type behind a bound. The caller sees only the bound.The bound must be a callable contract or an Effect (SEM0116).
Every reachable return must produce the same construction (SEM0113). Two different functions or literals don't unify.
Exact types#
typeof(name)is the exact type of the functionname. It is usable as a result type.The name must resolve to one ordinary function in scope (SEM0108, SEM0109, SEM0110).
A generic function's parameters must all be supplied (SEM0111).
typeof(id<i32>)names the instantiation of a generic function. Where that type is expected, the plain nameidis instantiated from it: as an argument (apply(id, 5)), a return value (return id) or an annotated initializer (let f: typeof(id<i32>) = id).A different function there is a type error: SEM0012 as an argument, SEM0129 as a return value, SEM0037 as an initializer.
id<i32>is not a value expression.return id<i32>is a parse error (PAR0002).
Examples#
struct Pair<A, B> {
first: A,
second: B,
}
union Maybe<T> {
None,
Some { value: T },
}
fn swap<A, B>(pair: Pair<A, B>) -> Pair<B, A> {
return Pair { first: move pair.second, second: move pair.first }
}
fn pick<A, B>(left: A, right: B) -> A {
return move left
}
fn orElse<T>(maybe: Maybe<T>, fallback: T) -> T {
return match move maybe {
Maybe<T>.Some { value } => move value
Maybe<T>.None => move fallback
}
}
pub fn main() -> i32 {
let swapped = swap(Pair { first: 1, second: true })
if !swapped.first || swapped.second != 1 {
return 1
}
if pick<i32>(40, false) != 40 {
return 2
}
if orElse(Maybe<i32>.Some { value: 42 }, 0) != 42 {
return 3
}
if orElse(Maybe<i32>.None, 7) != 7 {
return 4
}
return 0
}import silk.core.ops { Add }
import silk.core.order { Order }
struct Meters {
value: i32,
}
fn addMeters(left: Meters, right: Meters) -> Meters {
return Meters { value: left.value + right.value }
}
impl Add<Meters, Meters> for Meters {
add: addMeters
}
fn larger<T: Order>(left: T, right: T) -> T {
if (&left) < (&right) {
return move right
}
return move left
}
fn sumLarger<T: Add<T, T> + Order>(a: T, b: T, c: T) -> T {
return larger(move a, move b) + move c
}
fn add(value: i32, amount: i32) -> i32 {
return value + amount
}
fn adder(amount: i32) -> some<F: fn<'static>(i32) -> i32> F {
return add(amount)
}
fn exactAdd() -> typeof (add) {
return add
}
pub fn main() -> i32 {
if sumLarger(3, 9, 1) != 10 {
return 1
}
let total = Meters { value: 40 } + Meters { value: 2 }
if total.value != 42 {
return 2
}
if adder(40)(2) != 42 {
return 3
}
if exactAdd()(40, 2) != 42 {
return 4
}
return 0
}fn id<T>(value: T) -> T {
return move value
}
fn apply(f: typeof (id<i32>), value: i32) -> i32 {
return f(value)
}
fn pick() -> typeof (id<i32>) {
return id
}
pub fn main() -> i32 {
if apply(id, 5) != 5 {
return 1
}
if pick()(7) != 7 {
return 2
}
let f: typeof (id<i32>) = id
if f(9) != 9 {
return 3
}
return 0
}Two bounded inherent blocks on one generic struct. Vector2<bool> would have neither dot nor difference:
import silk.core.ownership { Copy }
import silk.core.ops { Add, Multiply, Subtract }
struct Vector2<T> {
x: T,
y: T,
}
impl<T: Copy> Copy for Vector2<T> {}
// `dot` exists where `T` adds and multiplies.
impl<T: Copy + Add<T, T> + Multiply<T, T>> Vector2<T> {
fn dot(self: Self, other: Self) -> T {
return self.x * other.x + self.y * other.y
}
}
// `difference` exists where `T` subtracts.
impl<T: Copy + Subtract<T, T>> Vector2<T> {
fn difference(self: Self, other: Self) -> Self {
return Vector2<T> { x: self.x - other.x, y: self.y - other.y }
}
}
pub fn main() -> i32 {
let a = Vector2<i32> { x: 3, y: 4 }
let b = Vector2<i32> { x: 1, y: 2 }
let gap = a.difference(b)
if a.dot(b) != 11 || gap.x != 2 {
return 1
}
let f = Vector2<f64> { x: 0.5, y: 2.0 }
if f.dot(f) != 4.25 {
return 2
}
return 0
}A generic body that uses an operator from conditional impls. mix's own bounds prove the bounds of both impls, so * and + on Point<T> are available. Leave Multiply<T, T> off mix and the first * is SEM0121:
import silk.core.ownership { Copy }
import silk.core.ops { Add, Multiply }
struct Point<T> {
x: T,
y: T,
}
impl<T: Copy> Copy for Point<T> {}
impl<T: Copy + Add<T, T>> Add<Point<T>, Point<T>> for Point<T> {
fn add(left: Self, right: Point<T>) -> Point<T> {
return Point<T> { x: left.x + right.x, y: left.y + right.y }
}
}
struct Scale<T> {
k: T,
}
impl<T: Copy> Copy for Scale<T> {}
impl<T: Copy + Multiply<T, T>> Multiply<Point<T>, Point<T>> for Scale<T> {
fn multiply(left: Self, right: Point<T>) -> Point<T> {
return Point<T> { x: left.k * right.x, y: left.k * right.y }
}
}
// `*` and `+` on `Point<T>` come from the conditional impls above. Their
// bounds hold because this function declares the same ones on `T`.
fn mix<T: Copy + Add<T, T> + Multiply<T, T>>(a: Point<T>, b: Point<T>, s: T, t: T) -> Point<T> {
return Scale<T> { k: s } * a + Scale<T> { k: t } * b
}
pub fn main() -> i32 {
let p = mix<i32>(Point<i32> { x: 1, y: 2 }, Point<i32> { x: 10, y: 20 }, 2, 3)
if p.x != 32 || p.y != 64 {
return 1
}
return 0
}With fn mix<T: Copy + Add<T, T>>, silk check prints:
src/main.silk:32:28: error[SEM0121] A concrete specialization whose conditional requirements cannot be provedA bounded inherent block that calls its bounds' operations, qualified and as a method:
interface Zero {
fn zero() -> Self
}
impl Zero for i32 {
fn zero() -> i32 { return 0 }
}
interface Twice {
fn twice(value: Self) -> Self
}
impl Twice for i32 {
fn twice(value: i32) -> i32 { return value * 2 }
}
struct Pair<T> {
x: T,
y: T,
}
impl<T: Zero + Twice> Pair<T> {
fn origin() -> Pair<T> {
return Pair<T> { x: Zero.zero(), y: Zero.zero() }
}
fn doubled(self: Self) -> Pair<T> {
return Pair<T> { x: Twice.twice(move self.x), y: self.y.twice() }
}
}
pub fn main() -> i32 {
let o = Pair<i32>.origin()
let d = Pair<i32> { x: 3, y: 4 }.doubled()
if o.x != 0 || d.x != 6 || d.y != 8 {
return 1
}
return 0
}Diagnostics#
| Code | Meaning |
|---|---|
| SEM0012 | Value argument contradicts an explicit type argument |
| SEM0037 | Initializer is a different function than the typeof annotation names |
| SEM0050 | Duplicate type parameter |
| SEM0051 | Wrong number of type arguments |
| SEM0052 | Cannot infer all type arguments from the supplied values |
| SEM0053 | Recursive call changes the type arguments (polymorphic recursion) |
| SEM0088 | Generic parameter used where a different kind is expected |
| SEM0099 | A type parameter, or a lifetime an explicit type argument leaves out, is left undetermined |
| SEM0108 | typeof names nothing in scope |
| SEM0109 | typeof name is ambiguous |
| SEM0110 | typeof names something other than an ordinary function |
| SEM0111 | typeof names a generic function without all its arguments |
| SEM0113 | Opaque result has more than one realization |
| SEM0116 | Opaque result bound is not a callable or Effect |
| SEM0129 | Returned function differs from the typeof result type |
| SEM0121 | Bound cannot be proved for the concrete arguments, or a type parameter lacks a bound its body needs |
| SEM0135 | Operator not available for the operand types |
| SEM0194 | Inherent impl head is not exactly the owner's parameters, or bounds one by a callable contract |
| SEM0221 | Owner type arguments placed on the member instead of the owner |
See also#
Interfaces and impls · Closures and callable types · Functions and methods · Lifetimes · Structs, tuples and arrays · Enums and unions · Static evaluation · Effects and run · Typed failures · Services and providers · silk/core/ops, silk/core/order, silk/core/ownership
Open questions#
SEM0100 (explicit type argument contradicts the arguments) exists in the catalog, but a contradiction such as
id<i32>(true)is reported as SEM0012 ("Expected i32 but received bool").