Reference / Generics
Generics
A generic declaration has type parameters, so that one definition works for many types. A use of it gives a type argument for each parameter.
class Box<T> {
value: T
}
fn first<T>(xs: [T]) -> T? {
if xs.len() > 0 { xs[0] } else { None }
}
fn main() {
let b = Box(value: "sauna")
print(b.value) // sauna
print(first([10, 20]) ?? 0) // 10
print(first(["Aino"]) ?? "none") // Aino
}
first works for an array whose elements have a type that a type argument can be (Type arguments). A function, a class and an enum can be generic. This page also tells what a generic body can do with a value of a type parameter, and which features are in development.
Type parameters
The type parameters of a declaration are names between < and > after the name of the declaration. A function, a class or an enum can have them.
class Pair<A, B> {
first: A
second: B
fn swap(self) -> Pair<B, A> {
Pair(first: self.second, second: self.first)
}
fn with_second<C>(self, second: C) -> Pair<A, C> {
Pair(first: self.first, second: second)
}
}
enum Tree<T> {
Leaf
Node(Tree<T>, T, Tree<T>)
}
fn size<T>(t: Tree<T>) -> i64 {
match t {
Leaf => 0
Node(l, _, r) => size(l) + 1 + size(r)
}
}
fn main() {
let p = Pair(first: 1, second: "one")
print(p.swap().first) // one
print(p.with_second(2.5).second) // 2.5
let t = Tree.Node(Tree.Leaf, 5, Tree.Node(Tree.Leaf, 6, Tree.Leaf))
print(size(t)) // 2
}
The rules:
- A type parameter is a type in the whole declaration. It can be the type of a parameter, a field or a payload, a return type, and a binding in the body.
- A method of a generic class or enum uses the type parameters of its type. A method can also add type parameters of its own, such as
Cabove. - The type parameters of one declaration have different names. A method cannot reuse a name from its type (E0034).
- A type parameter cannot take a name that E0026 lists, such as
i64orOption. mainhas no type parameters (E0029).
class Box<T> {
value: T
fn map<T>(self, x: T) -> T { // error[E0034]
x
}
}
fn main() {}
E0034: what this error means
Type arguments
A type argument is a type between < and > after the name of a generic class or enum. It can be any type that a value can have, such as i64, [string] or another generic type.
class Box<T> {
value: T
}
fn main() {
let a: Box<i64> = Box(value: 1)
let b: Box<Box<string>> = Box(value: Box(value: "sauna"))
let c: [Box<bool>] = [Box(value: true)]
let d: Box<f64>? = None
print(a.value) // 1
print(b.value.value) // sauna
print(c.len()) // 1
print(d == None) // true
}
The rules:
- A type that names a generic class or enum gives one type argument for each type parameter (E0137). As a type,
Boxalone andBox<i64, i64>are errors. - A class, an enum or a function that is not generic takes no type arguments (E0137).
- The type arguments of a variant go on its enum:
Tree<i64>.Node(...), notTree.Node<i64>(...)(E0137). - A type argument cannot be
unitornever(E0138). The type of a call that gives no value, such asprint(1), isunit. A type argument can be an optional, such asi64?inBox<i64?>. The section unit is in development, and it lifts the rule forunit.
class Box<T> {
value: T
}
fn main() {
let c: Box = Box(value: 1) // error[E0137]
}
E0137: what this error means
Inference
A call usually needs no type arguments. sisu finds each one from the arguments of the call, or from the type that the context expects. The context can be a let with a type, a parameter or a return type.
class Stack<T> {
mut items: [T]
}
fn empty<T>() -> Stack<T> {
Stack(items: [])
}
fn first<T>(xs: [T]) -> T? {
if xs.len() > 0 { xs[0] } else { None }
}
fn main() {
// T is i64, from the argument.
let a = Stack(items: [1, 2])
// T is string, from the type of b.
let b: Stack<string> = empty()
// T is written.
let c = empty<bool>()
print(a.items.len()) // 2
print(b.items.len()) // 0
print(c.items.len()) // 0
print(first<string>([]) ?? "empty") // empty
}
The rules:
- A type argument can be written in a call:
first<string>([]),Stack<i64>(items: []). A method call can write them too. - Only the arguments and the expected type give a type. A later use of the result does not.
- An argument such as
Ok(1), whose own arguments give only some of its types, takes the others from the outer call. Inlet r: Result<i64, string> = id(Ok(1)), the type ofrgivesTforid, andTgivesEforOk. - When an argument does not agree with the expected type, the error is at the argument.
let r: Result<i64, string> = Ok("x")reports"x": expectedi64, foundstring(E0032). With written type arguments, as inid<i64>(n), the error stays at the call. - A
Noneor a[]alone has no type. When nothing else fixes a type parameter, the call is an error (E0050). Annotate the result, or write the type arguments.
fn first<T>(xs: [T]) -> T? {
if xs.len() > 0 { xs[0] } else { None }
}
fn main() {
let x = first([]) // error[E0050]
}
E0050: what this error means
The < rule
After a name in an expression, < can start a list of type arguments. It does so only when a well-formed list follows and its closing > is followed by ( or .. Otherwise < is a comparison.
fn both(x: bool, y: bool) -> bool {
x && y
}
fn id<T>(x: T) -> T {
x
}
fn main() {
let a = 1
let b = 2
let c = 3
let d = 4
print(both(a < b, c > d)) // false
print(id<i64>(a) < id<i64>(b)) // true
}
So both(a < b, c > d) has two comparisons. In both(a < b, c > (d)), the < and the > read as a list of type arguments for a, and a is a value. This is an error (E0137). Put the first comparison in parentheses.
fn both(x: bool, y: bool) -> bool {
x && y
}
fn main() {
let a = 1
let b = 2
let c = 3
let d = 4
print(both(a < b, c > (d))) // error[E0137]
}
E0137: what this error means
Values of a type parameter
sisu checks a generic body once, where it is written, and not for each type argument. So it reports an error in a generic body also when no call uses the body.
In the body, the type parameter T can stand for any type. So the body can pass, return and store a value of type T, and it can use it in an optional or an array.
fn pick<T>(a: T, b: T, first: bool) -> T {
if first { a } else { b }
}
fn or_else<T>(x: T?, fallback: T) -> T {
x ?? fallback
}
fn present<T>(x: T?) -> bool {
x != None
}
fn main() {
print(pick("a", "b", false)) // b
print(or_else(None, 7)) // 7
print(or_else(3, 7)) // 3
print(present("x")) // true
}
The rules:
- An operator that computes with a
Tor compares it, such as+,<or==, is an error (E0139).containson a[T]uses==, so it is an error too.x != Noneandx == Noneon aT?are valid.x == yon twoT?is an error. !,&&and||take only abool, so aTthere is a type mismatch (E0032).- A
Thas no method and no field (E0063). isdoes not apply to aT, becauseTcan stand for a type that is not a class (E0046).printand interpolation do not take aT(E0059, E0098).[x; n]withx: Tis an error (E0067), becauseTcan be a class (Filled arrays).[None; n]is valid.matchdoes not take aT(E0124). In a payload of the typeT, a pattern is a name or_(Variant patterns).
fn smaller<T>(a: T, b: T) -> bool {
a < b // error[E0139]
}
fn main() {}
E0139: what this error means
The sections Bounds and Interfaces tell how a generic body can ask for more.
Growing type arguments
A generic function, class or enum cannot use itself with a type argument that grows, directly or through other declarations. nest([x], n - 1) in nest<T> calls nest with [T] for T, and then with [[T]], without end. This is an error (E0140).
fn nest<T>(x: T, n: i64) {
if n > 0 {
nest([x], n - 1) // error[E0140]
}
}
fn main() {
nest(1, 3)
}
E0140: what this error means
A use with the same type arguments, such as nest(x, n - 1), is valid.
Bounds In development
A bound says which interface a type argument must conform to. It follows the type parameter after a :. & joins two bounds, as in T: Shape & Ord.
fn largest<T: Ord>(xs: [T]) -> T? {
let mut best: T? = None
for x in xs {
if let b = best {
if b < x {
best = x
}
} else {
best = x
}
}
best
}
fn main() {
print(largest([3, 9, 2]) ?? 0) // 9
print(largest(["b", "a"]) ?? "") // b
}
The rules:
- In the body, a value of type
Tcan do what its bounds give it. These are the methods of the interfaces and the operators that they map to. Eqgives==and!=.Ordgives<,<=,>and>=.- Anything else is an error where the body is written. The help suggests the bound that is missing.
- A type argument that does not conform to a bound is an error at the call.
- Every type parameter can print, with no bound.
print(x),"{x}"andx.to_string()work on aT. - Every class conforms to
AnyObject. So does ananyvalue of a class-only interface (next rule). Nothing else does.T: AnyObjectlets the body useison values of typeT, and lets a class declare a weak field of typeT?. - An interface that inherits
AnyObjectis class-only. Itsanyvalues supportistoo, and a class can hold one in a weak field.
Interfaces In development
An interface lists the methods that a type must have. A class or an enum names its interfaces after its name. A method in an interface has self and no body.
interface Shape {
fn area(self) -> f64
}
class Circle: Shape {
r: f64
fn area(self) -> f64 {
f64.pi * self.r * self.r
}
}
fn total_area<T: Shape>(shapes: [T]) -> f64 {
let mut sum = 0.0
for s in shapes {
sum += s.area()
}
sum
}
fn main() {
print(total_area([Circle(r: 1.0), Circle(r: 2.0)]))
}
The rules:
- A type that names an interface must have each method of it, with the same signature. A missing method is an error.
- A type conforms to an interface with the same type arguments at most once.
Selfin an interface is the type that conforms to it, as infn eq(self, other: Self) -> bool.- An interface can require other interfaces:
interface Shape: Display. A type that conforms toShapeconforms toDisplaytoo. - Several interfaces follow a name, separated by
,:class Tree: Display, Eq. - A method in an interface can have type parameters of its own. Such an interface can bound a type parameter, but
anycannot take it.
Conformance after the declaration In development
extend adds methods to a type that is declared elsewhere. extend Type: Interface makes the type conform to the interface, with the methods of that interface in its body. One block names one interface.
class Celsius {
degrees: f64
}
class Fahrenheit {
degrees: f64
}
interface Transform<T> {
fn transform(self) -> T
}
extend Celsius: Transform<Fahrenheit> {
fn transform(self) -> Fahrenheit {
Fahrenheit(degrees: self.degrees * 1.8 + 32.0)
}
}
fn main() {
let f: Fahrenheit = Celsius(degrees: 100.0).transform()
print(f.degrees) // 212.0
}
The rules:
extend Type { ... }adds methods that follow no interface.- A program cannot extend a built-in type, such as
stringor[T]. - A generic interface, such as
Transform<T>, lets one type conform to it several times, with different type arguments. - When a call can use more than one conformance, the expected type picks one:
let f: Fahrenheit = c.transform(). Without an expected type, the call is an error.
Generic extensions In development
An extend can have type parameters. extend<T> Stack<T> adds methods to every Stack. extend Stack<i64> adds methods to Stack<i64> only.
class Stack<T> {
mut items: [T]
}
extend<T> Stack<T> {
fn size(self) -> i64 {
self.items.len()
}
}
extend<T: Eq> Stack<T>: Eq {
fn eq(self, other: Stack<T>) -> bool {
self.items == other.items
}
}
extend Stack<i64> {
fn total(self) -> i64 {
self.items.sum()
}
}
fn main() {
let a = Stack(items: [1, 2])
let b = Stack(items: [1, 2])
print(a.size()) // 2
print(a.total()) // 3
print(a == b) // true
}
The rules:
extend<T: Eq> Stack<T>: EqmakesStack<T>conform toEqonly whenTdoes.- Methods with the same name on different instantiations never overlap.
- One method name in two extensions that both cover an instantiation is an error. An example is
extend<T> Stack<T>andextend Stack<i64>, which both definetotal.
Associated types In development
An associated type is a type that each conforming type chooses once. The interface declares it with type. A conforming type sets it with type Name = Type.
interface Source {
type Item: Display
fn next(self) -> Item?
}
class Countdown: Source {
mut left: i64
type Item = i64
fn next(self) -> i64? {
if self.left == 0 {
return None
}
self.left -= 1
self.left + 1
}
}
fn first_item<S: Source>(s: S) -> S.Item? {
s.next()
}
fn show_next<S: Source>(s: S) {
if let x = s.next() {
print(x)
}
}
fn main() {
let c = Countdown(left: 2)
print(first_item(c) ?? 0) // 2
show_next(c) // 1
}
Inside the interface, the associated type is written Item. Elsewhere it is written through the type: S.Item for a type parameter S: Source.
An associated type can carry a bound, such as type Item: Display. Generic code can then print an S.Item, as show_next does.
Use an associated type when each type has one answer, such as the item of a Source. Use a type parameter, as in Transform<T>, when one type can conform in several ways. An interface can have both.
Display, Eq and Ord In development
Display, Eq and Ord are interfaces of the language. print and interpolation use Display. == and != use Eq. <, <=, > and >= use Ord.
class Point {
x: i64
y: i64
fn to_string(self) -> string {
"({self.x}, {self.y})"
}
}
class Money: Eq, Ord {
cents: i64
fn eq(self, other: Money) -> bool {
self.cents == other.cents
}
fn less(self, other: Money) -> bool {
self.cents < other.cents
}
}
enum Level: Eq {
Low
High
}
fn main() {
print(Point(x: 1, y: 2)) // (1, 2)
print(Money(cents: 5) == Money(cents: 5)) // true
print(Money(cents: 3) < Money(cents: 5)) // true
print(Level.Low == Level.High) // false
}
The rules:
- Every type that can be a type argument has a
Display. - A class prints its name, with its type arguments, and never its fields. A class with a method
to_stringprints the text of that method. - A class has no
==and no<. A class that conforms toEqhas==, and one that conforms toOrdhas<.a is bcompares two objects by identity. - A value that holds a class, such as an array of objects, has
==only when the class conforms toEq. f64conforms toEqandOrdwith the rules of IEEE 754. In generic code,nanis not equal tonan, as in other code.- An enum has
==when all its payloads have it, and it has<only when it conforms toOrd. - The methods that the interfaces require are
fn to_string(self) -> stringforDisplay,fn eq(self, other: Self) -> boolforEq, andfn less(self, other: Self) -> boolforOrd. A method namedto_stringwith another signature is an error. OrdrequiresEq. A type that conforms toOrdbut not toEqis an error.- For
Eq,a != bis!a.eq(b). - A conformance that a program writes replaces the generated one. An enum that writes
to_stringprints its own text. - A type can name a generated interface and write no method for it:
enum Level: Eqhas noeq. Where nothing is generated, such asclass Node: Eqwith noeq, a missing method is an error. - Only
to_stringconforms by name. A type with its owneqorlessmethod conforms toEqorOrdonly when its declaration names the interface. i64,u8,charandstringhaveEqandOrdwith no declaration. An enum hasEqwith no declaration when all its payloads have it.
Type aliases In development
A type alias gives a new name to a type. It is the same type as the type that it names, so no conversion is necessary.
type Grid = [[char?]]
type Pairs<T> = [[T]]
fn main() {
let g: Grid = [['a', None], [None, 'b']]
let p: Pairs<i64> = [[1, 2], [3, 4]]
print(g.len()) // 2
print(p[1][0]) // 3
}
The rules:
- A type alias is a top-level declaration. Its name must differ from the name of any function, class, enum or constant.
- An alias can have type parameters, such as
Pairs<T>. - An alias that refers to itself, directly or through another alias, is an error.
Interface values In development
any Shape is a type for a value of any type that conforms to Shape. A value converts to it where an any type is expected. A call of a method on it uses the method of the type that the value has. An interface that inherits AnyObject is class-only, as Bounds says.
interface Shape {
fn area(self) -> f64
}
class Circle: Shape {
r: f64
fn area(self) -> f64 {
f64.pi * self.r * self.r
}
}
class Rect: Shape {
w: f64
h: f64
fn area(self) -> f64 {
self.w * self.h
}
}
fn main() {
let shapes: [any Shape] = [Circle(r: 1.0), Rect(w: 2.0, h: 3.0)]
for s in shapes {
print(s.area())
}
}
The rules:
- An interface name alone is not a type. Write
T: Shapefor a type parameter, orany Shapefor a value. - An interface whose methods are generic, or that uses
Selfbesides the receiver, cannot beany. Soany Eqandany Ordare errors. - An
anyvalue prints through theDisplayof the type that it holds. It has no==and no<. - A generic function with the bound
T: Shapeaccepts an[any Shape].
Sisu