Browse the docs

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#

TEXT
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> { ... } and impl<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, Copy and 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 as impl<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() or Twice.twice(value), or as a method, as value.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 Self as a parameter, since Self is 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 in Holder<i32> for struct Holder<T, ?R>, is SEM0088 too; write Holder<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. Write List<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 in Option<&i32>.None with 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>(...), not Box.map<i32>(...) (SEM0221).

Bounds#

  • T: Interface requires a conformance of T to the interface. Bounds combine with +: T: Add<T, T> + Order.

  • T: 'a requires T to stay valid for 'a. T: 'static means T holds no borrows.

  • F: fn(i32) -> i32 bounds a parameter by a callable contract. See Closures and callable types.

  • T: Copy uses the sealed property from silk/core/ownership. Drop is 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.Named cannot be proved: a type parameter conforms only through its declared bounds; add main.Named to the bounds of H".

  • 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#

  • where follows the result and rows of a function. It holds membership and provider constraints. It does not hold T: Bound; write bounds inline.

  • where S in E says failure type S is a member of failure union E. where ?S in R does the same for requirement rows. See Typed failures and Services and providers.

  • where P provides X from Y relates 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: ?R stands for an effect requirement row, as in effect fn twice<?R>(step: Effect<'static; i32 ? R>) -> i32 ? R. See Services and providers.

  • Static value parameters are ordinary parameters marked static, as in fn 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]> from expand<T>, would need infinitely many instances and is rejected (SEM0053).

Opaque results#

  • -> some<F: Bound> F hides 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 function name. 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 name id is 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#

silk
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
}
silk
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
}
silk
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:

silk
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:

silk
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:

TEXT
src/main.silk:32:28: error[SEM0121] A concrete specialization whose conditional requirements cannot be proved

A bounded inherent block that calls its bounds' operations, qualified and as a method:

silk
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#

CodeMeaning
SEM0012Value argument contradicts an explicit type argument
SEM0037Initializer is a different function than the typeof annotation names
SEM0050Duplicate type parameter
SEM0051Wrong number of type arguments
SEM0052Cannot infer all type arguments from the supplied values
SEM0053Recursive call changes the type arguments (polymorphic recursion)
SEM0088Generic parameter used where a different kind is expected
SEM0099A type parameter, or a lifetime an explicit type argument leaves out, is left undetermined
SEM0108typeof names nothing in scope
SEM0109typeof name is ambiguous
SEM0110typeof names something other than an ordinary function
SEM0111typeof names a generic function without all its arguments
SEM0113Opaque result has more than one realization
SEM0116Opaque result bound is not a callable or Effect
SEM0129Returned function differs from the typeof result type
SEM0121Bound cannot be proved for the concrete arguments, or a type parameter lacks a bound its body needs
SEM0135Operator not available for the operand types
SEM0194Inherent impl head is not exactly the owner's parameters, or bounds one by a callable contract
SEM0221Owner 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").

View the source of this page