Generics
Abstractions · Lesson 33
A generic function or type is written once with a placeholder, T, and the compiler fills it in for each type you use. A bound such as T: PartialOrd says what T must be able to do, and the body may use only that.
Coming from Gofn largest<T: PartialOrd> is func largest[T cmp.Ordered], with the type parameter in angle brackets. The bound is a trait, so any type that implements PartialOrd, including your own structs, qualifies, not just the built-in ordered types.
Coming from Kotlin<T: PartialOrd> reads like <T : Comparable<T>>. Rust compiles a separate copy of the function for each type you use, so there is no type erasure and no reified keyword: TypeId::of::<T>() identifies a 'static type, and trait functions such as T::default() cover the rest.
Coming from Pythonfn largest<T: PartialOrd> is the 3.12 syntax def largest[T: Comparable], but the compiler enforces the bound. You can't use > on T unless the bound says it can. Python does not check hints when the program runs; a type checker such as mypy can, but only if you run one.
Coming from TypeScript<T: PartialOrd> is like <T extends ...>, but the bound is a trait (something the type can do), not a list of allowed types. Any type with PartialOrd, including your own struct with #[derive(PartialOrd)], can be passed in.
Coming from Javafn largest<T: PartialOrd> is <T extends Comparable<T>>, and you can use < and > directly. Rust generics are not erased: the compiler makes a separate copy for each type used, so T works with plain numbers without boxing.
Coming from C#<T: PartialOrd> is where T : IComparable<T>, and you can use < and > instead of CompareTo. Like C#, Rust does not erase T, but it gets there differently: the compiler makes a separate copy of the function for each type, so no T exists at run time (C# keeps it, and typeof(T) works).
Coming from CC has no generics, so you'd write largest_int and largest_str by hand or paste text with a macro. Rust writes those copies for you and type-checks them.
Coming from C++Generics are templates with a concept, T: PartialOrd playing the role of std::totally_ordered T, and each type gets its own compiled copy. The difference is that Rust checks the function body against the bound once, so a missing bound is reported at the definition. A concept checks the call, not the body: a + it never promised still fails per type, inside the body.
The quick comparison
// T can be any type that can be compared with < and >fn largest<T: PartialOrd>(items: &[T]) -> &T { let mut best = &items[0]; for item in items { if item > best { best = item; } } best}
fn main() { println!("{}", largest(&[3, 9, 2])); println!("{}", largest(&["kiwi", "apple", "pear"]));}9 pear
package main
import ( "cmp" "fmt")
func largest[T cmp.Ordered](items []T) T { best := items[0] for _, item := range items { if item > best { best = item } } return best}
func main() { fmt.Println(largest([]int{3, 9, 2})) fmt.Println(largest([]string{"kiwi", "apple", "pear"}))}9 pear
fun <T : Comparable<T>> largest(items: List<T>): T { var best = items[0] for (item in items) { if (item > best) { best = item } } return best}
fun main() { println(largest(listOf(3, 9, 2))) println(largest(listOf("kiwi", "apple", "pear")))}9 pear
from typing import Protocol, Self
class Comparable(Protocol): def __gt__(self, other: Self, /) -> bool: ...
def largest[T: Comparable](items: list[T]) -> T: best = items[0] for item in items: if item > best: best = item return best
print(largest([3, 9, 2]))print(largest(["kiwi", "apple", "pear"]))9 pear
// T can be a number or a string: both can be compared with < and >function largest<T extends number | string>(items: readonly T[]): T { let best = items[0]; for (const item of items) { if (item > best) { best = item; } } return best;}
console.log(largest([3, 9, 2]));console.log(largest(["kiwi", "apple", "pear"]));9 pear
import java.util.List;
public class Main { // T can be any type that can be compared to itself static <T extends Comparable<T>> T largest(List<T> items) { T best = items.get(0); for (T item : items) { if (item.compareTo(best) > 0) { best = item; } } return best; }
public static void main(String[] args) { System.out.println(largest(List.of(3, 9, 2))); System.out.println(largest(List.of("kiwi", "apple", "pear"))); }}9 pear
Console.WriteLine(Largest([3, 9, 2]));Console.WriteLine(Largest(["kiwi", "apple", "pear"]));
// T can be any type that can compare itself to another Tstatic T Largest<T>(T[] items) where T : IComparable<T>{ var best = items[0]; foreach (var item in items) { if (item.CompareTo(best) > 0) { best = item; } } return best;}9 pear
#include <stdio.h>#include <string.h>
// C has no generics: you write one version per type.const int *largest_int(const int *items, size_t len) { const int *best = &items[0]; for (size_t i = 0; i < len; i++) { if (items[i] > *best) best = &items[i]; } return best;}
const char *const *largest_str(const char *const *items, size_t len) { const char *const *best = &items[0]; for (size_t i = 0; i < len; i++) { if (strcmp(items[i], *best) > 0) best = &items[i]; } return best;}
int main(void) { int nums[] = {3, 9, 2}; const char *fruits[] = {"kiwi", "apple", "pear"}; printf("%d\n", *largest_int(nums, 3)); printf("%s\n", *largest_str(fruits, 3)); return 0;}9 pear
#include <concepts>#include <iostream>#include <string_view>#include <vector>
// T can be any type that can be compared with < and >template <std::totally_ordered T>const T& largest(const std::vector<T>& items) { const T* best = &items[0]; for (const T& item : items) { if (item > *best) { best = &item; } } return *best;}
int main() { std::vector<int> nums{3, 9, 2}; std::vector<std::string_view> fruit{"kiwi", "apple", "pear"}; std::cout << largest(nums) << "\n"; std::cout << largest(fruit) << "\n";}9 pear
What's different
T: PartialOrdmeans "any typeTwhose values can be compared with<and>". Without it, the compiler won't let you compare twoTvalues.- This returns
&T, a reference to the biggest item insideitems, so nothing is copied. - Several bounds:
T: PartialOrd + Display. Long lists go in awhereclause after the signature. - Rust makes you state the constraint up front.
largest<T>with no bound is a compile error, because the compiler checks a generic function on its own, before anyone calls it.
Adding two Ts and returning a T
Looks fine: in Go, Python, C#, C and C++ it runs fine. Rust won't compile it.
Works in Go. In Rust it's just a rule.Kotlin refuses this too. Same idea, different words.Works in Python. In Rust it's just a rule.TypeScript refuses this too. Same idea, different words.Java refuses this too. Same idea, different words.Works in C#. In Rust it's just a rule.Works in C. In Rust it's just a rule.Works in C++. In Rust it's just a rule.use std::ops::Add;
fn add<T: Add>(a: T, b: T) -> T { a + b}
fn main() { println!("{}", add(2, 3)); println!("{}", add(1.5, 2.25));}package main
import "fmt"
// T is one of these types, and `+` on a T gives a T.func add[T int | float64](a, b T) T { return a + b}
func main() { fmt.Println(add(2, 3)) fmt.Println(add(1.5, 2.25))}5 3.75
Kotlin refuses too. Number has no plus, so a + b on a T : Number finds nothing to call.
// Number has no `plus`, so there is no way to say "any type with +".fun <T : Number> add(a: T, b: T): T = a + b
fun main() { println(add(2, 3)) println(add(1.5, 2.25))}Main.kt:2:41: error: none of the following candidates is applicable:
fun BigDecimal.plus(other: BigDecimal): BigDecimal:
Candidate 'fun BigDecimal.plus(other: BigDecimal): BigDecimal' is inapplicable because of a receiver type mismatch.
fun BigInteger.plus(other: BigInteger): BigInteger:
Candidate 'fun BigInteger.plus(other: BigInteger): BigInteger' is inapplicable because of a receiver type mismatch.
fun String?.plus(other: Any?): String:
Candidate 'fun String?.plus(other: Any?): String' is inapplicable because of a receiver type mismatch.
fun <T> Array<T>.plus(element: T): Array<T>:
Candidate 'fun <T> Array<T>.plus(element: T): Array<T>' is inapplicable because of a receiver type mismatch.
fun ByteArray.plus(element: Byte): ByteArray:
Candidate 'fun ByteArray.plus(element: Byte): ByteArray' is inapplicable because of a receiver type mismatch.
fun ShortArray.plus(element: Short): ShortArray:
Candidate 'fun ShortArray.plus(element: Short): ShortArray' is inapplicable because of a receiver type mismatch.
fun IntArray.plus(element: Int): IntArray:
Candidate 'fun IntArray.plus(element: Int): IntArray' is inapplicable because of a receiver type mismatch.
fun LongArray.plus(element: Long): LongArray:
Candidate 'fun LongArray.plus(element: Long): LongArray' is inapplicable because of a receiver type mismatch.
fun FloatArray.plus(element: Float): FloatArray:
Candidate 'fun FloatArray.plus(element: Float): FloatArray' is inapplicable because of a receiver type mismatch.
fun DoubleArray.plus(element: Double): DoubleArray:
Candidate 'fun DoubleArray.plus(element: Double): DoubleArray' is inapplicable because of a receiver type mismatch.
fun BooleanArray.plus(element: Boolean): BooleanArray:
Candidate 'fun BooleanArray.plus(element: Boolean): BooleanArray' is inapplicable because of a receiver type mismatch.
fun CharArray.plus(element: Char): CharArray:
Candidate 'fun CharArray.plus(element: Char): CharArray' is inapplicable because of a receiver type mismatch.
fun <T> Array<T>.plus(elements: Collection<T>): Array<T>:
Candidate 'fun <T> Array<T>.plus(elements: Collection<T>): Array<T>' is inapplicable because of a receiver type mismatch.
fun ByteArray.plus(elements: Collection<Byte>): ByteArray:
Candidate 'fun ByteArray.plus(elements: Collection<Byte>): ByteArray' is inapplicable because of a receiver type mismatch.
fun ShortArray.plus(elements: Collection<Short>): ShortArray:
Candidate 'fun ShortArray.plus(elements: Collection<Short>): ShortArray' is inapplicable because of a receiver type mismatch.
fun IntArray.plus(elements: Collection<Int>): IntArray:
Candidate 'fun IntArray.plus(elements: Collection<Int>): IntArray' is inapplicable because of a receiver type mismatch.
fun LongArray.plus(elements: Collection<Long>): LongArray:
Candidate 'fun LongArray.plus(elements: Collection<Long>): LongArray' is inapplicable because of a receiver type mismatch.
fun FloatArray.plus(elements: Collection<Float>): FloatArray:
Candidate 'fun FloatArray.plus(elements: Collection<Float>): FloatArray' is inapplicable because of a receiver type mismatch.
fun DoubleArray.plus(elements: Collection<Double>): DoubleArray:
Candidate 'fun DoubleArray.plus(elements: Collection<Double>): DoubleArray' is inapplicable because of a receiver type mismatch.
fun BooleanArray.plus(elements: Collection<Boolean>): BooleanArray:
Candidate 'fun BooleanArray.plus(elements: Collection<Boolean>): BooleanArray' is inapplicable because of a receiver type mismatch.
fun CharArray.plus(elements: Collection<Char>): CharArray:
Candidate 'fun CharArray.plus(elements: Collection<Char>): CharArray' is inapplicable because of a receiver type mismatch.
fun <T> Array<T>.plus(elements: Array<out T>): Array<T>:
Candidate 'fun <T> Array<T>.plus(elements: Array<out T>): Array<T>' is inapplicable because of a receiver type mismatch.
fun ByteArray.plus(elements: ByteArray): ByteArray:
Candidate 'fun ByteArray.plus(elements: ByteArray): ByteArray' is inapplicable because of a receiver type mismatch.
fun ShortArray.plus(elements: ShortArray): ShortArray:
Candidate 'fun ShortArray.plus(elements: ShortArray): ShortArray' is inapplicable because of a receiver type mismatch.
fun IntArray.plus(elements: IntArray): IntArray:
Candidate 'fun IntArray.plus(elements: IntArray): IntArray' is inapplicable because of a receiver type mismatch.
fun LongArray.plus(elements: LongArray): LongArray:
Candidate 'fun LongArray.plus(elements: LongArray): LongArray' is inapplicable because of a receiver type mismatch.
fun FloatArray.plus(elements: FloatArray): FloatArray:
Candidate 'fun FloatArray.plus(elements: FloatArray): FloatArray' is inapplicable because of a receiver type mismatch.
fun DoubleArray.plus(elements: DoubleArray): DoubleArray:
Candidate 'fun DoubleArray.plus(elements: DoubleArray): DoubleArray' is inapplicable because of a receiver type mismatch.
fun BooleanArray.plus(elements: BooleanArray): BooleanArray:
Candidate 'fun BooleanArray.plus(elements: BooleanArray): BooleanArray' is inapplicable because of a receiver type mismatch.
fun CharArray.plus(elements: CharArray): CharArray:
Candidate 'fun CharArray.plus(elements: CharArray): CharArray' is inapplicable because of a receiver type mismatch.
fun <T> Iterable<T>.plus(element: T): List<T>:
Candidate 'fun <T> Iterable<T>.plus(element: T): List<T>' is inapplicable because of a receiver type mismatch.
fun <T> Collection<T>.plus(element: T): List<T>:
Candidate 'fun <T> Collection<T>.plus(element: T): List<T>' is inapplicable because of a receiver type mismatch.
fun <T> Iterable<T>.plus(elements: Array<out T>): List<T>:
Candidate 'fun <T> Iterable<T>.plus(elements: Array<out T>): List<T>' is inapplicable because of a receiver type mismatch.
fun <T> Collection<T>.plus(elements: Array<out T>): List<T>:
Candidate 'fun <T> Collection<T>.plus(elements: Array<out T>): List<T>' is inapplicable because of a receiver type mismatch.
fun <T> Iterable<T>.plus(elements: Iterable<T>): List<T>:
Candidate 'fun <T> Iterable<T>.plus(elements: Iterable<T>): List<T>' is inapplicable because of a receiver type mismatch.
fun <T> Collection<T>.plus(elements: Iterable<T>): List<T>:
Candidate 'fun <T> Collection<T>.plus(elements: Iterable<T>): List<T>' is inapplicable because of a receiver type mismatch.
fun <T> Iterable<T>.plus(elements: Sequence<T>): List<T>:
Candidate 'fun <T> Iterable<T>.plus(elements: Sequence<T>): List<T>' is inapplicable because of a receiver type mismatch.
fun <T> Collection<T>.plus(elements: Sequence<T>): List<T>:
Candidate 'fun <T> Collection<T>.plus(elements: Sequence<T>): List<T>' is inapplicable because of a receiver type mismatch.
fun <K, V> Map<out K, V>.plus(pair: Pair<K, V>): Map<K, V>:
Candidate 'fun <K, V> Map<out K, V>.plus(pair: Pair<K, V>): Map<K, V>' is inapplicable because of a receiver type mismatch.
fun <K, V> Map<out K, V>.plus(pairs: Iterable<Pair<K, V>>): Map<K, V>:
Candidate 'fun <K, V> Map<out K, V>.plus(pairs: Iterable<Pair<K, V>>): Map<K, V>' is inapplicable because of a receiver type mismatch.
fun <K, V> Map<out K, V>.plus(pairs: Array<out Pair<K, V>>): Map<K, V>:
Candidate 'fun <K, V> Map<out K, V>.plus(pairs: Array<out Pair<K, V>>): Map<K, V>' is inapplicable because of a receiver type mismatch.
fun <K, V> Map<out K, V>.plus(pairs: Sequence<Pair<K, V>>): Map<K, V>:
Candidate 'fun <K, V> Map<out K, V>.plus(pairs: Sequence<Pair<K, V>>): Map<K, V>' is inapplicable because of a receiver type mismatch.
fun <K, V> Map<out K, V>.plus(map: Map<out K, V>): Map<K, V>:
Candidate 'fun <K, V> Map<out K, V>.plus(map: Map<out K, V>): Map<K, V>' is inapplicable because of a receiver type mismatch.
fun <T> Set<T>.plus(element: T): Set<T>:
Candidate 'fun <T> Set<T>.plus(element: T): Set<T>' is inapplicable because of a receiver type mismatch.
fun <T> Set<T>.plus(elements: Array<out T>): Set<T>:
Candidate 'fun <T> Set<T>.plus(elements: Array<out T>): Set<T>' is inapplicable because of a receiver type mismatch.
fun <T> Set<T>.plus(elements: Iterable<T>): Set<T>:
Candidate 'fun <T> Set<T>.plus(elements: Iterable<T>): Set<T>' is inapplicable because of a receiver type mismatch.
fun <T> Set<T>.plus(elements: Sequence<T>): Set<T>:
Candidate 'fun <T> Set<T>.plus(elements: Sequence<T>): Set<T>' is inapplicable because of a receiver type mismatch.
fun UIntArray.plus(element: UInt): UIntArray:
Candidate 'fun UIntArray.plus(element: UInt): UIntArray' is inapplicable because of a receiver type mismatch.
fun ULongArray.plus(element: ULong): ULongArray:
Candidate 'fun ULongArray.plus(element: ULong): ULongArray' is inapplicable because of a receiver type mismatch.
fun UByteArray.plus(element: UByte): UByteArray:
Candidate 'fun UByteArray.plus(element: UByte): UByteArray' is inapplicable because of a receiver type mismatch.
fun UShortArray.plus(element: UShort): UShortArray:
Candidate 'fun UShortArray.plus(element: UShort): UShortArray' is inapplicable because of a receiver type mismatch.
fun UIntArray.plus(elements: Collection<UInt>): UIntArray:
Candidate 'fun UIntArray.plus(elements: Collection<UInt>): UIntArray' is inapplicable because of a receiver type mismatch.
fun ULongArray.plus(elements: Collection<ULong>): ULongArray:
Candidate 'fun ULongArray.plus(elements: Collection<ULong>): ULongArray' is inapplicable because of a receiver type mismatch.
fun UByteArray.plus(elements: Collection<UByte>): UByteArray:
Candidate 'fun UByteArray.plus(elements: Collection<UByte>): UByteArray' is inapplicable because of a receiver type mismatch.
fun UShortArray.plus(elements: Collection<UShort>): UShortArray:
Candidate 'fun UShortArray.plus(elements: Collection<UShort>): UShortArray' is inapplicable because of a receiver type mismatch.
fun UIntArray.plus(elements: UIntArray): UIntArray:
Candidate 'fun UIntArray.plus(elements: UIntArray): UIntArray' is inapplicable because of a receiver type mismatch.
fun ULongArray.plus(elements: ULongArray): ULongArray:
Candidate 'fun ULongArray.plus(elements: ULongArray): ULongArray' is inapplicable because of a receiver type mismatch.
fun UByteArray.plus(elements: UByteArray): UByteArray:
Candidate 'fun UByteArray.plus(elements: UByteArray): UByteArray' is inapplicable because of a receiver type mismatch.
fun UShortArray.plus(elements: UShortArray): UShortArray:
Candidate 'fun UShortArray.plus(elements: UShortArray): UShortArray' is inapplicable because of a receiver type mismatch.
fun <T> Sequence<T>.plus(element: T): Sequence<T>:
Candidate 'fun <T> Sequence<T>.plus(element: T): Sequence<T>' is inapplicable because of a receiver type mismatch.
fun <T> Sequence<T>.plus(elements: Array<out T>): Sequence<T>:
Candidate 'fun <T> Sequence<T>.plus(elements: Array<out T>): Sequence<T>' is inapplicable because of a receiver type mismatch.
fun <T> Sequence<T>.plus(elements: Iterable<T>): Sequence<T>:
Candidate 'fun <T> Sequence<T>.plus(elements: Iterable<T>): Sequence<T>' is inapplicable because of a receiver type mismatch.
fun <T> Sequence<T>.plus(elements: Sequence<T>): Sequence<T>:
Candidate 'fun <T> Sequence<T>.plus(elements: Sequence<T>): Sequence<T>' is inapplicable because of a receiver type mismatch.
fun Char.plus(other: String): String:
Candidate 'fun Char.plus(other: String): String' is inapplicable because of a receiver type mismatch.
fun <T : Number> add(a: T, b: T): T = a + b
^def add[T](a: T, b: T) -> T: return a + b
print(add(2, 3))print(add(1.5, 2.25))5 3.75
TypeScript refuses too, in terms close to Rust's: the sum is a number, and T could be a narrower type, so it cannot be returned as a T.
function add<T extends number>(a: T, b: T): T { return a + b;}
console.log(add(2, 3));console.log(add(1.5, 2.25));main.ts(2,3): error TS2322: Type 'number' is not assignable to type 'T'. 'number' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'number'.
Java refuses too (bad operand types for binary operator '+'). Number has no +, and a type variable cannot supply one.
public class Main { // Number has no `+`: Java's operators work only on the primitive types. static <T extends Number> T add(T a, T b) { return a + b; }
public static void main(String[] args) { System.out.println(add(2, 3)); System.out.println(add(1.5, 2.25)); }}Main.java:4: error: bad operand types for binary operator '+'
return a + b;
^
first type: T
second type: T
where T is a type-variable:
T extends Number declared in method <T>add(T,T)
1 errorusing System.Numerics;
Console.WriteLine(Add(2, 3));Console.WriteLine(Add(1.5, 2.25));
// INumber<T> (C# 11) says "T has + and gives a T back".static T Add<T>(T a, T b) where T : INumber<T>{ return a + b;}5 3.75
#include <stdio.h>
// C has no generics. A macro pastes the text, and the compiler checks the result.#define ADD(a, b) ((a) + (b))
int main(void) { printf("%d\n", ADD(2, 3)); printf("%g\n", ADD(1.5, 2.25)); return 0;}5 3.75
#include <iostream>
// No constraint: the body is checked when add is used with a type.template <typename T>T add(T a, T b) { return a + b;}
int main() { std::cout << add(2, 3) << "\n"; std::cout << add(1.5, 2.25) << "\n";}5 3.75
What does the compiler say? Guess first, then open.
error[E0308]: mismatched types --> src/main.rs:4:5 | 3 | fn add<T: Add>(a: T, b: T) -> T { | - - expected `T` because of return type | | | expected this type parameter 4 | a + b | ^^^^^ expected type parameter `T`, found associated type | = note: expected type parameter `T` found associated type `<T as Add>::Output` help: consider further restricting this bound | 3 | fn add<T: Add<Output = T>>(a: T, b: T) -> T { | ++++++++++++
Go prints 5 and 3.75. The constraint int | float64 lists the allowed types, and + on a T gives a T back, so nothing more needs to be said.
Kotlin refuses too. Number has no plus, so a + b on a T : Number finds nothing to call.
Python prints 5 and 3.75. The interpreter does not check the [T] hint when the program runs, so a + b is tried with whatever arrives. A type checker such as mypy would reject a + b on an unbounded T; this site does not run one.
TypeScript refuses too, in terms close to Rust's: the sum is a number, and T could be a narrower type, so it cannot be returned as a T.
Java refuses too (bad operand types for binary operator '+'). Number has no +, and a type variable cannot supply one.
C# prints 5 and 3.75. The INumber<T> constraint (C# 11) says T has + and that adding two T gives a T, so the promise is in the constraint.
C prints 5 and 3.75. The macro pastes ((2) + (3)) and ((1.5) + (2.25)) before the compiler looks at types, so there is no generic function to check.
C++ prints 5 and 3.75. With no constraint, the template body is checked when it is used with int and with double. A C++20 concept would check the call, not the body. Rust checks the body once, against the bound.
T: Add says only that two T values can be added. The type of the sum is a separate type, <T as Add>::Output, and nothing says it is T. So the compiler refuses to return it as a T.
In Rust, + calls the add method of the Add trait, and each implementation chooses its own result type. For i32 it is i32, but for &i32 it is also i32, not &i32. Add<Output = T> promises that the sum is a T, and the compiler's help: writes it for you. The toolkit below prints the types.
The fix
Compiles and runs.
use std::ops::Add;
// The sum of two Ts must also be a T: `Output = T` says so.fn add<T: Add<Output = T>>(a: T, b: T) -> T { a + b}
fn main() { println!("{}", add(2, 3)); println!("{}", add(1.5, 2.25));}5 3.75
The toolkit
Generic functions
A generic function is written once with a placeholder for a type, usually T, and the compiler fills it in at each call. Reading signatures shows where the <T> goes; this lesson is about what it does.
// T is a placeholder: the compiler fills it in at each call.fn first_and_last<T>(items: &[T]) -> Option<(&T, &T)> { Some((items.first()?, items.last()?))}
// T appears only in the result, so the caller has to say which one.fn zero<T: Default>() -> T { T::default()}
fn main() { println!("numbers: {:?}", first_and_last(&[4, 8, 15, 16])); println!("words: {:?}", first_and_last(&["ant", "bee", "cat"])); println!("nothing: {:?}", first_and_last::<f64>(&[])); println!("zero::<i32>(): {}", zero::<i32>()); let name: String = zero(); println!("zero() as a String: {name:?}");}numbers: Some((4, 16))
words: Some(("ant", "cat"))
nothing: None
zero::<i32>(): 0
zero() as a String: ""Tis worked out from the arguments.&[4, 8, 15, 16]makes iti32, and the words make it&str.- It returns references into the slice, not
Titself: a genericTmay not beCopy, and moving an item out of a slice is refused (Reading signatures runs E0508). - An empty list says nothing about
T, sofirst_and_last::<f64>(&[])names it with the turbofish (::<>). T: Defaultis a bound: “Tmust have a default value”. Bounds get their own section below.
zero mentions T only in its result, so a call with nothing to go on is an error:
fn zero<T: Default>() -> T { T::default()}
fn main() { let n = zero(); println!("{n}");}error[E0283]: type annotations needed --> src/main.rs:6:9 | 6 | let n = zero(); | ^ ------ type must be known at this point | = note: the type must implement `Default` note: required by a bound in `zero` --> src/main.rs:1:12 | 1 | fn zero<T: Default>() -> T { | ^^^^^^^ required by this bound in `zero` help: consider giving `n` an explicit type | 6 | let n: /* Type */ = zero(); | ++++++++++++
help: offers a type on the let; zero::<i32>() does the same job. This is E0283. Vec::new() with no use gave E0282 in Declarations and types; both are the “type annotations needed” family.
Generic structs and enums
Structs and enums take type parameters the same way. Vec<T>, Option<T> and Result<T, E> are ordinary types declared like this, and Maybe<T> below has the shape of Option<T>.
#[derive(Debug)]struct Pair<T> { first: T, second: T,}
// `impl<T>` declares T, then `Pair<T>` uses it: every Pair gets these methods.impl<T> Pair<T> { fn new(first: T, second: T) -> Pair<T> { Pair { first, second } }
fn swapped(self) -> Pair<T> { Pair::new(self.second, self.first) }
// A method can add parameters of its own, after its name. fn map<U>(self, f: impl Fn(T) -> U) -> Pair<U> { Pair::new(f(self.first), f(self.second)) }}
// No T here: this method exists only on Pair<f64>.impl Pair<f64> { fn sum(&self) -> f64 { self.first + self.second }}
// Two parameters, which may be different types.struct Entry<K, V> { key: K, value: V,}
#[derive(Debug)]enum Maybe<T> { Nothing, Just(T),}
fn main() { println!("ints: {:?}", Pair::new(1, 2).swapped()); println!("mapped: {:?}", Pair::new(1, 2).map(|n| format!("#{n}"))); println!("words: {:?}", Pair::new("a", "b")); println!("sum: {}", Pair::new(1.5, 2.0).sum()); let entry = Entry { key: "id", value: 7, }; println!("entry: {} -> {}", entry.key, entry.value); let found: Maybe<char> = Maybe::Just('x'); let missing: Maybe<char> = Maybe::Nothing; println!("maybe: {found:?} and {missing:?}");}ints: Pair { first: 2, second: 1 }
mapped: Pair { first: "#1", second: "#2" }
words: Pair { first: "a", second: "b" }
sum: 3.5
entry: id -> 7
maybe: Just('x') and Nothing- In
impl<T> Pair<T>, the first<T>introduces the placeholder and the second says which type the block is for. - A method can add parameters of its own after its name:
map<U>turns aPair<T>into aPair<U>, andimpl Fn(T) -> Utakes a closure. impl Pair<f64>names one concrete type, sosumexists only for pairs off64.Entry<K, V>has two parameters, the same type or different ones.
Bounds: what T may do
Inside a generic body, T could be any type, so a bare T has no {}, no == and no .clone(): the body can move it, borrow it and store it. Anything more must be asked for in a bound, the name of a trait (an ability a type can have):
// Nothing is asked of T, so a body may not print it or compare it.fn bracket<T>(item: T) -> String { format!("[{item}]")}
fn same<T>(a: T, b: T) -> bool { a == b}
fn main() { println!("{} {}", bracket(5), same(1, 2));}error[E0277]: `T` doesn't implement `std::fmt::Display` --> src/main.rs:3:15 | 3 | format!("[{item}]") | ^^^^^^ `T` cannot be formatted with the default formatter | = note: in format strings you may be able to use `{:?}` (or `{:#?}` for pretty-print) instead help: consider restricting type parameter `T` with trait `Display` | 2 | fn bracket<T: std::fmt::Display>(item: T) -> String { | +++++++++++++++++++ error[E0369]: binary operation `==` cannot be applied to type `T` --> src/main.rs:7:7 | 7 | a == b | - ^^ - T | | | T | help: consider restricting type parameter `T` with trait `PartialEq` | 6 | fn same<T: std::cmp::PartialEq>(a: T, b: T) -> bool { | +++++++++++++++++++++ Some errors have detailed explanations: E0277, E0369.
The help: writes the bound for you. These are the ones you will ask for most; Common traits says what each means.
To do this with a T |
Ask for |
|---|---|
print with {}, with {:?} |
Display, Debug |
compare with ==; with < and > |
PartialEq; PartialOrd |
sort(), max(), use as a BTreeSet item |
Ord |
use as a HashMap key or HashSet item |
Eq + Hash |
call .clone() |
Clone |
| use a value twice without moving it | Copy |
a + b giving a T |
Add<Output = T> |
T::default() |
Default |
turn a u8 or u32 into a u64 |
Into<u64> |
f64 has PartialOrd but not Ord: NaN is neither less than nor greater than anything, so sort() on floats is refused (Common traits). Leave a needed bound out and the error is inside the body; pass a type that lacks it and the error is at the call (Reading signatures shows both).
a + b is the odd row, and the gotcha above is about it. T: Add says two Ts can be added, but the sum has a type of its own, T::Output, and for &i32 it is i32:
use std::any::type_name;use std::ops::Add;
// `T: Add` says two Ts can be added. The result type is T::Output.fn sum_type<T: Add>() -> String { format!( "T = {:<5} T + T = {}", type_name::<T>(), type_name::<T::Output>() )}
fn main() { println!("{}", sum_type::<i32>()); println!("{}", sum_type::<f64>()); println!("{}", sum_type::<&i32>());}T = i32 T + T = i32 T = f64 T + T = f64 T = &i32 T + T = i32
Add<Output = T> closes the gap: “adding two Ts gives a T”.
Several bounds, where and impl Trait
use std::fmt::{Debug, Display};
// Two bounds joined with `+`.fn report<T: PartialOrd + Display>(items: &[T]) -> String { let mut best = &items[0]; for item in items { if item > best { best = item; } } format!("largest of {}: {best}", items.len())}
// Two `impl` parameters are two separate hidden type parameters.fn pair_impl(a: impl Display, b: impl Display) -> String { format!("{a} and {b}")}
// Each parameter can have its own bounds; `where` keeps the signature readable.fn both<A, B>(shown: A, debugged: B) -> Stringwhere A: Display, B: Debug,{ format!("{shown} and {debugged:?}")}
fn main() { println!("{}", report(&[3, 9, 2])); println!("{}", report(&["kiwi", "apple", "pear"])); println!("pair_impl: {}", pair_impl(1, "one")); println!("both: {}", both("text", vec![Some(1), None]));}largest of 3: 9 largest of 3: pear pair_impl: 1 and one both: text and [Some(1), None]
+ asks for all the bounds, and where moves them after the signature (Traits shows all three forms). Each impl parameter is its own hidden type parameter, so pair_impl(1, "one") works. With one T for both the arguments must match:
use std::fmt::Display;
// One T for both parameters: the two arguments must have the same type.fn pair<T: Display>(a: T, b: T) -> String { format!("{a} and {b}")}
fn main() { println!("{}", pair(1, "one"));}error[E0308]: mismatched types --> src/main.rs:9:28 | 9 | println!("{}", pair(1, "one")); | ---- - ^^^^^ expected integer, found `&str` | | | | | expected all arguments to be this integer type because they need to match the type of this parameter | arguments to this function are incorrect | note: function defined here --> src/main.rs:4:4 | 4 | fn pair<T: Display>(a: T, b: T) -> String { | ^^^^ - ---- ---- this parameter needs to match the integer type of `a` | | | | | `b` needs to match the integer type of this parameter | `a` and `b` both reference this parameter `T`
A hidden parameter has no name to write, so the turbofish refuses it:
use std::fmt::Display;
fn named<T: Display>(x: T) -> String { format!("{x}")}
fn hidden(x: impl Display) -> String { format!("{x}")}
fn main() { println!("{}", named::<i32>(1)); println!("{}", hidden::<i32>(1));}error[E0107]: function takes 0 generic arguments but 1 generic argument was supplied --> src/main.rs:13:20 | 13 | println!("{}", hidden::<i32>(1)); | ^^^^^^------- help: remove the unnecessary generics | | | expected 0 generic arguments | note: function defined here, with 0 generic parameters --> src/main.rs:7:4 | 7 | fn hidden(x: impl Display) -> String { | ^^^^^^ = note: `impl Trait` cannot be explicitly specified as a generic argument
Use T when parameters must match or callers may need the turbofish, impl when you never name the type.
Methods for some T only
A bound can sit on an impl block, so its methods exist only for types that qualify:
use std::fmt::Display;
struct Wrapper<T>(T);
// For every T.impl<T> Wrapper<T> { fn into_inner(self) -> T { self.0 }}
// Only when T can be displayed.impl<T: Display> Wrapper<T> { fn show(&self) -> String { format!("[{}]", self.0) }}
fn main() { println!("number: {}", Wrapper(42).show()); println!("text: {}", Wrapper("hi").show()); let list = Wrapper(vec![1, 2]); println!("a list still unwraps: {:?}", list.into_inner());}number: [42] text: [hi] a list still unwraps: [1, 2]
A list has no show; the error says a bound is missing and points at the impl that asks for it:
use std::fmt::Display;
struct Wrapper<T>(T);
impl<T: Display> Wrapper<T> { fn show(&self) -> String { format!("[{}]", self.0) }}
fn main() { println!("{}", Wrapper(vec![1, 2]).show());}error[E0599]: the method `show` exists for struct `Wrapper<Vec<{integer}>>`, but its trait bounds were not satisfied --> src/main.rs:12:40 | 3 | struct Wrapper<T>(T); | ----------------- method `show` not found for this struct ... 12 | println!("{}", Wrapper(vec![1, 2]).show()); | ^^^^ method cannot be called on `Wrapper<Vec<{integer}>>` due to unsatisfied trait bounds | note: trait bound `Vec<{integer}>: std::fmt::Display` was not satisfied --> src/main.rs:5:9 | 5 | impl<T: Display> Wrapper<T> { | ^^^^^^^ ---------- | | | unsatisfied trait bound introduced here
Put bounds on the impl that needs them, not on the struct. A bound on the struct is demanded of every impl that mentions it, even one whose methods do not use it:
use std::fmt::Display;
// The bound is on the struct, so every impl has to repeat it.struct Labeled<T: Display> { value: T,}
impl<T> Labeled<T> { fn value(&self) -> &T { &self.value }}
fn main() { let label = Labeled { value: 7 }; println!("{}", label.value());}error[E0277]: `T` doesn't implement `std::fmt::Display` --> src/main.rs:8:9 | 8 | impl<T> Labeled<T> { | ^^^^^^^^^^ the trait `std::fmt::Display` is not implemented for `T` | note: required by a bound in `Labeled` --> src/main.rs:4:19 | 4 | struct Labeled<T: Display> { | ^^^^^^^ required by this bound in `Labeled` help: consider restricting type parameter `T` with trait `Display` | 8 | impl<T: std::fmt::Display> Labeled<T> { | +++++++++++++++++++ error[E0277]: `T` doesn't implement `std::fmt::Display` --> src/main.rs:9:15 | 9 | fn value(&self) -> &T { | ^^^^ the trait `std::fmt::Display` is not implemented for `T` | note: required by a bound in `Labeled` --> src/main.rs:4:19 | 4 | struct Labeled<T: Display> { | ^^^^^^^ required by this bound in `Labeled` help: consider restricting type parameter `T` with trait `Display` | 8 | impl<T: std::fmt::Display> Labeled<T> { | +++++++++++++++++++
The API Guidelines agree: Data structures do not duplicate derived trait bounds.
The same works for a trait: impl<T: Display> Shout for T gives Shout to every type that meets the bound, types written later too. This blanket impl runs in Traits; ToString is one.
Traits with their own parameters
A trait can take type parameters too, and one type can implement it once for each:
// A trait can take a type parameter too.trait Convert<T> { fn convert(&self) -> T;}
struct Celsius(f64);
impl Convert<f64> for Celsius { fn convert(&self) -> f64 { self.0 * 9.0 / 5.0 + 32.0 }}
impl Convert<String> for Celsius { fn convert(&self) -> String { format!("{} degrees C", self.0) }}
fn main() { let temp = Celsius(100.0); // The type you ask for picks the impl. let fahrenheit: f64 = temp.convert(); let label: String = temp.convert(); println!("as f64: {fahrenheit}"); println!("as String: {label}");}as f64: 212 as String: 100 degrees C
The type you ask for picks the impl.
A parameter can have a default. Add is Add<Rhs = Self>, so impl Add for Meters means Add<Meters>, and a bare T: Add means Add<T>, which is why the gotcha can add two Ts.
use std::ops::Add;
#[derive(Debug, Clone, Copy)]struct Meters(f64);
// `Add` is declared `Add<Rhs = Self>`: a type parameter with a default.// Writing `Add` alone means `Add<Meters>`.impl Add for Meters { type Output = Meters; fn add(self, other: Meters) -> Meters { Meters(self.0 + other.0) }}
// The same type can implement it again for another right-hand side.impl Add<f64> for Meters { type Output = Meters; fn add(self, other: f64) -> Meters { Meters(self.0 + other) }}
fn main() { println!("Meters + Meters: {:?}", Meters(1.0) + Meters(2.0)); println!("Meters + f64: {:?}", Meters(1.0) + 0.5);}Meters + Meters: Meters(3.0) Meters + f64: Meters(1.5)
When a type should have only one answer, a trait uses an associated type instead, as Add does for Output and Iterator does for Item (Traits).
Const generics
A const generic is a number, not a type, known when the program is compiled. It is how a function or type works with arrays of any length:
use std::any::type_name;
// N is a number the compiler knows, filled in from the array's type.fn sum<const N: usize>(values: [i32; N]) -> i32 { println!(" an array of {N}"); values.iter().sum()}
struct Buffer<const N: usize> { data: [u8; N],}
impl<const N: usize> Buffer<N> { fn new() -> Buffer<N> { Buffer { data: [0; N] } }
fn capacity(&self) -> usize { self.data.len() }}
fn main() { println!("sum: {}", sum([1, 2, 3])); println!("sum: {}", sum([10, 20, 30, 40, 50])); println!("sum of none: {}", sum([])); println!("capacity: {}", Buffer::<16>::new().capacity()); println!("type of [i32; 3]: {}", type_name::<[i32; 3]>()); println!("type of [i32; 4]: {}", type_name::<[i32; 4]>());}an array of 3 sum: 6 an array of 5 sum: 150 an array of 0 sum of none: 0 capacity: 16 type of [i32; 3]: [i32; 3] type of [i32; 4]: [i32; 4]
[i32; 3] and [i32; 4] are different types, so sum takes the length as a parameter. N can be used as a value inside, and 0 is allowed. A const parameter may be an integer, char or bool (Reference). For a length known only at run time, use a slice or a Vec (Vectors).
Your language’s generics
Pick your language in “Coming from” at the top. Every row below is compiled in the image that builds this page, in Rust and in the other language.
Going deeper
One copy per type
The compiler writes a separate copy of a generic function for each type it is used with. This is called monomorphization. Each copy knows its T: size_of::<T>() is a plain number in it, and < or + is the operation for that type, chosen when compiling. type_name::<T>() below is for diagnostics only: the standard library says its text is not specified.
use std::any::type_name;use std::mem::size_of;
fn describe<T>() { println!( "T = {:<30} size_of::<T>() = {}", type_name::<T>(), size_of::<T>() );}
fn largest<T: PartialOrd>(items: &[T]) -> &T { let mut best = &items[0]; for item in items { if item > best { best = item; } } best}
fn main() { // The same function body, once per T, and each copy knows its T. describe::<u8>(); describe::<u64>(); describe::<String>(); describe::<&str>(); describe::<Option<u32>>();
let for_ints = largest::<i32> as *const (); let for_floats = largest::<f64> as *const (); println!( "largest::<i32> and largest::<f64> are different functions: {}", for_ints != for_floats );}T = u8 size_of::<T>() = 1 T = u64 size_of::<T>() = 8 T = alloc::string::String size_of::<T>() = 24 T = &str size_of::<T>() = 16 T = core::option::Option<u32> size_of::<T>() = 8 largest::<i32> and largest::<f64> are different functions: true
The last line compares the addresses of two instances of largest: different functions. The Book says the result runs as if you had written each copy by hand (Performance of Code Using Generics). The price is more compiled code, one copy per type. This page measures no speed, binary size or compile time, and the runner builds only debug builds.
Because a copy knows its T, a generic struct stores its fields inline:
use std::mem::size_of;
struct Pair<T> { first: T, second: T,}
fn main() { println!("size_of::<i32>(): {}", size_of::<i32>()); println!("size_of::<Pair<i32>>(): {}", size_of::<Pair<i32>>()); println!("size_of::<String>(): {}", size_of::<String>()); println!("size_of::<Pair<String>>(): {}", size_of::<Pair<String>>());
let numbers = Pair { first: 1, second: 2, }; let names = Pair { first: String::from("ana"), second: String::from("bo"), }; println!("numbers: {} and {}", numbers.first, numbers.second); println!("names: {} and {}", names.first, names.second); println!( "first name: len {}, capacity {}", names.first.len(), names.first.capacity() ); println!( "second name: len {}, capacity {}", names.second.len(), names.second.capacity() );}size_of::<i32>(): 4 size_of::<Pair<i32>>(): 8 size_of::<String>(): 24 size_of::<Pair<String>>(): 48 numbers: 1 and 2 names: ana and bo first name: len 3, capacity 3 second name: len 2, capacity 2
T has a known size, unless you say otherwise
Each copy must know how big its T is, so every type parameter carries a hidden bound, Sized. The text behind a &str is a str, whose length is known only when the program runs, so it cannot be a T:
use std::fmt::Display;
fn show<T: Display>(item: &T) -> String { format!("<{item}>")}
fn main() { println!("{}", show("hello"));}error[E0277]: the size for values of type `str` cannot be known at compilation time --> src/main.rs:8:25 | 8 | println!("{}", show("hello")); | ---- ^^^^^^^ doesn't have a size known at compile-time | | | required by a bound introduced by this call | = help: the trait `Sized` is not implemented for `str` note: required by an implicit `Sized` bound in `show` --> src/main.rs:3:9 | 3 | fn show<T: Display>(item: &T) -> String { | ^ required by the implicit `Sized` requirement on this type parameter in `show` help: consider relaxing the implicit `Sized` restriction | 3 | fn show<T: Display + ?Sized>(item: &T) -> String { | ++++++++
?Sized (“may or may not have a known size”) lifts the rule. A & has a known size, so a reference to the value is fine:
use std::fmt::Display;use std::mem::size_of_val;
// `?Sized` lifts the hidden "T has a known size" rule.fn show<T: Display + ?Sized>(item: &T) -> String { format!("<{item}> is {} bytes", size_of_val(item))}
fn main() { println!("{}", show("hello")); println!("{}", show(&5_i64));}<hello> is 5 bytes <5> is 8 bytes
You will meet T: ?Sized in standard library signatures: ToString is implemented for every T: Display + ?Sized.
What T is at run time, in your language
In Rust each copy is for one concrete type, so no generic code is left at run time and there is no reflection on T: you cannot list its members or rely on its name. You can ask whether two types are the same, for 'static types (types that hold no borrowed data), and you call T’s behaviour through trait functions such as T::default():
use std::any::{Any, TypeId};
// `T: 'static` means T holds no borrowed data. TypeId works for such types.fn same_type<A: 'static, B: 'static>() -> bool { TypeId::of::<A>() == TypeId::of::<B>()}
fn is_a<T: 'static>(value: &dyn Any) -> bool { value.is::<T>()}
fn main() { println!("same_type::<i32, i32>(): {}", same_type::<i32, i32>()); println!("same_type::<i32, u8>(): {}", same_type::<i32, u8>()); println!("is_a::<i32>(&3i32): {}", is_a::<i32>(&3i32)); println!("is_a::<i32>(&\"x\"): {}", is_a::<i32>(&"x"));}same_type::<i32, i32>(): true same_type::<i32, u8>(): false is_a::<i32>(&3i32): true is_a::<i32>(&"x"): false
Other languages keep more or less of T:
When to make something generic
Write the concrete version first. Make it generic when a second type needs the same code, and ask for only the bounds the body uses: every extra bound narrows who can call it, as the exercises check.
Generics or trait objects
A generic parameter is one type per use. Vec<T> holds only Ts, and a list of mixed types is refused:
fn main() { let items = vec![1, "two", 3.0]; println!("{}", items.len());}error[E0308]: mismatched types --> src/main.rs:2:25 | 2 | let items = vec![1, "two", 3.0]; | ^^^^^ expected integer, found `&str`
When one list must hold different types that share an ability, you use a trait object (dyn Trait): the compiler no longer picks a copy, and the method is looked up while the program runs. Trait objects covers it, and what it costs compared with generics.
Practice
Try it
Each exercise is a small program with checks in main. Open it in the Playground, replace the todo!(), and run until it prints all checks passed.
Smallest and largest
Return references to the smallest and the largest item of a slice, or None if it is empty. The checks use whole numbers, decimals, &str, char and a Version type that supports < and > and nothing else. Decimals are not Ord, and all of them are negative, so starting from a zero does not work either.
// The smallest and the largest item of a slice, or None if it is empty.// The checks use whole numbers, decimals, &str, char and a type with almost no traits.fn min_max<T>(items: &[T]) -> Option<(&T, &T)> { todo!("the compiler will name the bound you need; start from the first item")}
// Comparable with < and >, and nothing else: no Copy, Clone, Debug or Ord.#[derive(PartialEq, PartialOrd)]struct Version(u32, u32);
fn main() { assert_eq!(min_max(&[3, 9, 2, 7]), Some((&2, &9))); assert_eq!(min_max(&[1, 5, 9]), Some((&1, &9))); assert_eq!(min_max(&[9, 5, 1]), Some((&1, &9))); assert_eq!(min_max(&[4]), Some((&4, &4))); // Decimals can be compared with < but are not Ord; all negative on purpose. assert_eq!(min_max(&[-4.5, -0.5, -9.25]), Some((&-9.25, &-0.5))); assert_eq!( min_max(&["kiwi", "apple", "pear"]), Some((&"apple", &"pear")) ); assert_eq!(min_max(&['z', 'a']), Some((&'a', &'z'))); assert_eq!(min_max::<i32>(&[]), None);
let versions = [Version(1, 4), Version(0, 9), Version(2, 0), Version(1, 10)]; let (lowest, highest) = min_max(&versions).unwrap(); assert_eq!((lowest.0, lowest.1), (0, 9)); assert_eq!((highest.0, highest.1), (2, 0)); // The answer is an item of the slice itself, not a copy. assert!(std::ptr::eq(lowest, &versions[1])); println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
// The smallest and the largest item of a slice, or None if it is empty.// The checks use whole numbers, decimals, &str, char and a type with almost no traits.fn min_max<T: PartialOrd>(items: &[T]) -> Option<(&T, &T)> { let mut smallest = items.first()?; let mut largest = smallest; for item in items { if item < smallest { smallest = item; } if item > largest { largest = item; } } Some((smallest, largest))}
// Comparable with < and >, and nothing else: no Copy, Clone, Debug or Ord.#[derive(PartialEq, PartialOrd)]struct Version(u32, u32);
fn main() { assert_eq!(min_max(&[3, 9, 2, 7]), Some((&2, &9))); assert_eq!(min_max(&[1, 5, 9]), Some((&1, &9))); assert_eq!(min_max(&[9, 5, 1]), Some((&1, &9))); assert_eq!(min_max(&[4]), Some((&4, &4))); // Decimals can be compared with < but are not Ord; all negative on purpose. assert_eq!(min_max(&[-4.5, -0.5, -9.25]), Some((&-9.25, &-0.5))); assert_eq!( min_max(&["kiwi", "apple", "pear"]), Some((&"apple", &"pear")) ); assert_eq!(min_max(&['z', 'a']), Some((&'a', &'z'))); assert_eq!(min_max::<i32>(&[]), None);
let versions = [Version(1, 4), Version(0, 9), Version(2, 0), Version(1, 10)]; let (lowest, highest) = min_max(&versions).unwrap(); assert_eq!((lowest.0, lowest.1), (0, 9)); assert_eq!((highest.0, highest.1), (2, 0)); // The answer is an item of the slice itself, not a copy. assert!(std::ptr::eq(lowest, &versions[1])); println!("all checks passed");}all checks passed
A stack of anything
Fill in Stack<T>: push, pop, peek and len, plus show, which prints the items as [1, 2, 3]. The checks push more than three items and include a type that cannot be printed.
// A stack of any type: the last item pushed is the first one popped.use std::fmt::Display;
struct Stack<T> { items: Vec<T>,}
impl<T> Stack<T> { fn new() -> Stack<T> { todo!("an empty stack") }
fn push(&mut self, item: T) { todo!("add to the end of items") }
fn pop(&mut self) -> Option<T> { todo!("take from the end of items") }
fn peek(&self) -> Option<&T> { todo!("look at the end of items without taking it") }
fn len(&self) -> usize { todo!("how many items") }
// Looks like "[1, 2, 3]", bottom of the stack first. fn show(&self) -> String { todo!("join the items with \", \" inside brackets") }}
// No Display, Clone or Debug: a Stack must still hold it.struct Token { id: u32,}
fn main() { let mut numbers = Stack::new(); assert_eq!(numbers.pop(), None); numbers.push(1); numbers.push(2); numbers.push(3); assert_eq!(numbers.len(), 3); assert_eq!(numbers.peek(), Some(&3)); assert_eq!(numbers.len(), 3); assert_eq!(numbers.show(), "[1, 2, 3]"); assert_eq!(numbers.pop(), Some(3)); assert_eq!(numbers.show(), "[1, 2]"); for n in 3..=6 { numbers.push(n); } assert_eq!(numbers.len(), 6); assert_eq!(numbers.show(), "[1, 2, 3, 4, 5, 6]");
let mut words = Stack::new(); words.push(String::from("ana")); words.push(String::from("bo")); assert_eq!(words.show(), "[ana, bo]"); assert_eq!(words.pop(), Some(String::from("bo")));
let empty: Stack<char> = Stack::new(); assert_eq!(empty.show(), "[]");
let mut tokens = Stack::new(); tokens.push(Token { id: 7 }); assert_eq!(tokens.peek().map(|t| t.id), Some(7)); let token = tokens.pop().unwrap(); assert_eq!(token.id, 7); assert_eq!(tokens.len(), 0); println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
// A stack of any type: the last item pushed is the first one popped.use std::fmt::Display;
struct Stack<T> { items: Vec<T>,}
impl<T> Stack<T> { fn new() -> Stack<T> { Stack { items: Vec::new() } }
fn push(&mut self, item: T) { self.items.push(item); }
fn pop(&mut self) -> Option<T> { self.items.pop() }
fn peek(&self) -> Option<&T> { self.items.last() }
fn len(&self) -> usize { self.items.len() }}
// Only stacks of displayable items get `show`.impl<T: Display> Stack<T> { fn show(&self) -> String { let mut text = String::from("["); for (i, item) in self.items.iter().enumerate() { if i > 0 { text.push_str(", "); } text.push_str(&item.to_string()); } text.push(']'); text }}
// No Display, Clone or Debug: a Stack must still hold it.struct Token { id: u32,}
fn main() { let mut numbers = Stack::new(); assert_eq!(numbers.pop(), None); numbers.push(1); numbers.push(2); numbers.push(3); assert_eq!(numbers.len(), 3); assert_eq!(numbers.peek(), Some(&3)); assert_eq!(numbers.len(), 3); assert_eq!(numbers.show(), "[1, 2, 3]"); assert_eq!(numbers.pop(), Some(3)); assert_eq!(numbers.show(), "[1, 2]"); for n in 3..=6 { numbers.push(n); } assert_eq!(numbers.len(), 6); assert_eq!(numbers.show(), "[1, 2, 3, 4, 5, 6]");
let mut words = Stack::new(); words.push(String::from("ana")); words.push(String::from("bo")); assert_eq!(words.show(), "[ana, bo]"); assert_eq!(words.pop(), Some(String::from("bo")));
let empty: Stack<char> = Stack::new(); assert_eq!(empty.show(), "[]");
let mut tokens = Stack::new(); tokens.push(Token { id: 7 }); assert_eq!(tokens.peek().map(|t| t.id), Some(7)); let token = tokens.pop().unwrap(); assert_eq!(token.id, 7); assert_eq!(tokens.len(), 0); println!("all checks passed");}all checks passed
The first N items
Return the first N items of a slice as an array, filled up with the default value when the slice is shorter. The caller picks N with a type: let a: [i32; 3] = first_n(&items). The checks use numbers, String and a type with very few traits: choose the bounds the body needs and no more.
// The first N items of a slice as an array; if the slice is shorter, the rest is// the default value. The caller picks N, usually with a type: `let a: [i32; 3] = ...`.fn first_n<T, const N: usize>(items: &[T]) -> [T; N] { todo!("`std::array::from_fn(|i| ...)` builds an array of length N")}
// A type with very few traits: no Copy, no Debug, no PartialEq.#[derive(Clone, Default)]struct Tally(u32);
fn main() { let a: [i32; 3] = first_n(&[1, 2, 3, 4, 5]); assert_eq!(a, [1, 2, 3]); let b: [i32; 4] = first_n(&[7]); assert_eq!(b, [7, 0, 0, 0]); let c: [char; 3] = first_n(&['x', 'y', 'z']); assert_eq!(c, ['x', 'y', 'z']); let none: [char; 0] = first_n(&['x']); assert_eq!(none, []); // Strings are Clone but not Copy; the default is the empty string. let d: [String; 2] = first_n(&[String::from("ana")]); assert_eq!(d, ["ana", ""]); let e: [Tally; 2] = first_n(&[Tally(5)]); assert_eq!((e[0].0, e[1].0), (5, 0)); // N can also be named with the turbofish. assert_eq!(first_n::<f64, 2>(&[1.5, 2.5, 3.5]), [1.5, 2.5]); println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
// The first N items of a slice as an array; if the slice is shorter, the rest is// the default value. The caller picks N, usually with a type: `let a: [i32; 3] = ...`.fn first_n<T: Clone + Default, const N: usize>(items: &[T]) -> [T; N] { std::array::from_fn(|i| items.get(i).cloned().unwrap_or_default())}
// A type with very few traits: no Copy, no Debug, no PartialEq.#[derive(Clone, Default)]struct Tally(u32);
fn main() { let a: [i32; 3] = first_n(&[1, 2, 3, 4, 5]); assert_eq!(a, [1, 2, 3]); let b: [i32; 4] = first_n(&[7]); assert_eq!(b, [7, 0, 0, 0]); let c: [char; 3] = first_n(&['x', 'y', 'z']); assert_eq!(c, ['x', 'y', 'z']); let none: [char; 0] = first_n(&['x']); assert_eq!(none, []); // Strings are Clone but not Copy; the default is the empty string. let d: [String; 2] = first_n(&[String::from("ana")]); assert_eq!(d, ["ana", ""]); let e: [Tally; 2] = first_n(&[Tally(5)]); assert_eq!((e[0].0, e[1].0), (5, 0)); // N can also be named with the turbofish. assert_eq!(first_n::<f64, 2>(&[1.5, 2.5, 3.5]), [1.5, 2.5]); println!("all checks passed");}all checks passed
Read more
- The Rust Book, chapter 10.1: Generic Data Types, which ends with monomorphization, and 10.2: Defining Shared Behavior for bounds,
whereand blanket impls. - Rust by Example: Generics, with Bounds and Where clauses.
- The Rust Reference, Generic parameters, including const generics.
- The standard library:
std::ops::Add(theOutputtype),std::any::TypeIdandstd::any::type_name.
Learn Rust is an independent project, not affiliated with the Rust Project or the Rust Foundation. Code and text are under the MIT licence.Source on GitHub.