Rust Notes: Generics
A review of Generics in Rust.
Generics in Rust
Generics are Rust’s mechanism for writing code that works with multiple types without duplicating the implementation.
Instead of writing:
1
2
3
fn largest_i32(list: &[i32]) -> &i32 {
// ...
}
and another function for f64:
1
2
3
fn largest_f64(list: &[f64]) -> &f64 {
// ...
}
we can write one generic function:
1
2
3
fn largest<T>(list: &[T]) -> &T {
// ...
}
Here T is a generic type parameter.
Generics are one of the foundations of Rust’s type system and appear together with:
- ownership
- borrowing
- lifetimes
- traits
- trait bounds
- associated types
impl Trait- trait objects
- const generics
- static dispatch
- monomorphization
The most important idea is:
Generics let us write code in terms of a type that will be specified later.
1. Why Do We Need Generics?
Imagine we want a function that returns the first element of a slice.
Without generics:
1
2
3
4
5
6
7
8
9
10
11
fn first_i32(values: &[i32]) -> &i32 {
&values[0]
}
fn first_string(values: &[String]) -> &String {
&values[0]
}
fn first_f64(values: &[f64]) -> &f64 {
&values[0]
}
The implementations are identical.
Only the type changes.
Generics allow us to write:
1
2
3
fn first<T>(values: &[T]) -> &T {
&values[0]
}
Now the same function works with:
1
2
3
4
5
6
7
8
let numbers = vec![1, 2, 3];
let strings = vec![
String::from("hello"),
String::from("world"),
];
let n = first(&numbers);
let s = first(&strings);
The compiler determines T from the arguments.
2. What Is T?
In:
1
2
3
fn first<T>(values: &[T]) -> &T {
&values[0]
}
T is a type parameter.
Think of it as a placeholder:
1
2
3
T
│
└── some type
For example, when calling:
1
first(&numbers)
the compiler can infer:
1
T = i32
When calling:
1
first(&strings)
it can infer:
1
T = String
Conceptually:
1
2
first<i32>(&[i32])
first<String>(&[String])
3. Generic Syntax
The basic syntax is:
1
<T>
For example:
1
2
fn foo<T>(value: T) {
}
Multiple type parameters:
1
2
fn foo<T, U>(x: T, y: U) {
}
A generic struct:
1
2
3
4
struct Pair<T, U> {
first: T,
second: U,
}
A generic enum:
1
2
3
4
enum Result<T, E> {
Ok(T),
Err(E),
}
A generic trait:
1
2
trait Container<T> {
}
4. Generic Functions
The most common syntax is:
1
2
3
fn identity<T>(value: T) -> T {
value
}
Usage:
1
2
let x = identity(10);
let y = identity("hello");
The compiler infers:
1
T = i32
for the first call and:
1
T = &str
for the second.
5. Explicit Generic Arguments
Sometimes you can specify the generic type explicitly.
For example:
1
2
3
fn identity<T>(value: T) -> T {
value
}
You can write:
1
let x = identity::<i32>(10);
The syntax:
1
::<T>
is called turbofish syntax.
For multiple types:
1
2
3
4
5
fn pair<T, U>(x: T, y: U) -> (T, U) {
(x, y)
}
let value = pair::<i32, String>(10, String::from("hello"));
6. Why Is It Called Turbofish?
This:
1
::<T>
is commonly called the turbofish.
For example:
1
foo::<i32>();
The :: followed by <...> makes the syntax visually resemble a fish.
You will frequently see it with generic methods and functions:
1
"42".parse::<i32>();
Here:
1
2
3
parse::<i32>()
↑
explicitly choose T = i32
7. Type Inference
Rust usually does not require explicit generic arguments.
For example:
1
2
3
4
5
fn identity<T>(value: T) -> T {
value
}
let x = identity(10);
Rust sees:
1
10 → i32
and therefore infers:
1
T = i32
You can explicitly specify it:
1
let x = identity::<i32>(10);
Both are valid.
Idiomatic Rust generally relies on inference when it is obvious.
8. Generic Structs
A struct can have generic type parameters:
1
2
3
4
struct Point<T> {
x: T,
y: T,
}
Now:
1
2
3
4
5
6
7
8
9
let integer_point = Point {
x: 10,
y: 20,
};
let float_point = Point {
x: 1.5,
y: 2.5,
};
Conceptually:
1
2
Point<i32>
Point<f64>
9. Different Types in the Same Struct
You can use multiple generic parameters:
1
2
3
4
struct Point<T, U> {
x: T,
y: U,
}
Now:
1
2
3
4
let point = Point {
x: 10,
y: 3.14,
};
Conceptually:
1
Point<i32, f64>
This is useful when the fields do not need to have the same type.
10. Generic Enums
Rust’s standard library uses generics extensively.
For example:
1
2
3
4
enum Option<T> {
Some(T),
None,
}
This allows:
1
2
3
Option<i32>
Option<String>
Option<&str>
For example:
1
2
3
let x: Option<i32> = Some(10);
let y: Option<String> = Some(String::from("hello"));
The enum itself does not need separate implementations for every type.
11. Result<T, E>
Another important generic type is:
1
Result<T, E>
It represents:
1
2
T → success value
E → error value
For example:
1
Result<String, std::io::Error>
or:
1
Result<i32, ParseIntError>
This is a powerful example of why generics are useful.
The same Result abstraction works with many combinations of success and error types.
12. Generic Methods
A struct can be generic and its methods can also be generic.
For example:
1
2
3
4
5
6
7
8
9
struct Container<T> {
value: T,
}
impl<T> Container<T> {
fn value(&self) -> &T {
&self.value
}
}
Notice:
1
impl<T> Container<T>
The first T declares the generic parameter.
The second T uses it.
13. Why Do We Need impl<T>?
Suppose:
1
2
3
struct Container<T> {
value: T,
}
To implement methods for all T:
1
2
3
4
5
impl<T> Container<T> {
fn value(&self) -> &T {
&self.value
}
}
Read it as:
For every type
T, implement these methods forContainer<T>.
14. Implementing Only for One Concrete Type
You can also specialize the implementation for one concrete type:
1
2
3
4
5
6
7
8
9
struct Container<T> {
value: T,
}
impl Container<i32> {
fn double(&self) -> i32 {
self.value * 2
}
}
Now double() exists only for:
1
Container<i32>
It does not exist for:
1
Container<String>
15. Generic Implementation with a Different Type Parameter
You can introduce additional generic parameters inside impl.
For example:
1
2
3
4
5
6
7
8
9
struct Container<T> {
value: T,
}
impl<T> Container<T> {
fn pair<U>(self, other: U) -> (T, U) {
(self.value, other)
}
}
Usage:
1
2
3
let c = Container { value: 10 };
let result = c.pair("hello");
The types are:
1
2
T = i32
U = &str
16. Generic Functions and Generic Methods Are Different
Consider:
1
2
3
4
5
6
7
8
9
10
11
12
13
struct Container<T> {
value: T,
}
impl<T> Container<T> {
fn get(&self) -> &T {
&self.value
}
fn convert<U>(self, value: U) -> (T, U) {
(self.value, value)
}
}
Here:
1
T
belongs to the struct.
While:
1
U
belongs only to the method.
17. Generic Traits
Traits can also be generic:
1
2
3
trait Converter<T> {
fn convert(&self) -> T;
}
A type can implement the trait for a particular T:
1
2
3
4
5
6
7
struct Number;
impl Converter<i32> for Number {
fn convert(&self) -> i32 {
42
}
}
18. Trait Generic Parameters vs Associated Types
This is an important Rust distinction.
A generic trait:
1
2
3
trait Iterator<T> {
fn next(&mut self) -> Option<T>;
}
would allow one type to implement the trait multiple times for different T.
Rust’s actual Iterator uses an associated type:
1
2
3
4
5
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
This means a particular implementation chooses one Item type.
For example:
1
2
3
4
5
6
7
impl Iterator for MyIterator {
type Item = i32;
fn next(&mut self) -> Option<Self::Item> {
// ...
}
}
19. Why Associated Types Matter
With:
1
2
3
trait Iterator {
type Item;
}
we can write:
1
2
3
4
5
6
fn sum<I>(iter: I) -> i32
where
I: Iterator<Item = i32>,
{
// ...
}
The Item = i32 syntax constrains the associated type.
This is different from:
1
I: Iterator<i32>
because Iterator does not have a generic parameter.
20. Trait Bounds
A generic type can have restrictions.
For example:
1
2
3
fn print<T: std::fmt::Display>(value: T) {
println!("{value}");
}
The:
1
T: Display
is a trait bound.
It says:
Tmust implementDisplay.
Without this bound, Rust cannot assume that T supports {value} formatting.
21. Why Do Generic Types Need Bounds?
Consider:
1
2
3
fn add<T>(a: T, b: T) -> T {
a + b
}
This does not compile.
Why?
Because Rust cannot assume that every type supports:
1
+
For example:
1
2
3
4
String
File
TcpStream
MyCustomType
do not automatically have addition semantics.
We need to specify the required capability.
22. Using a Trait Bound
For a type that supports addition:
1
2
3
4
5
6
7
8
use std::ops::Add;
fn add<T>(a: T, b: T) -> T
where
T: Add<Output = T>,
{
a + b
}
Now the compiler knows that T implements Add.
This demonstrates an important idea:
Generics describe what can vary; trait bounds describe what the generic type must be able to do.
23. Multiple Trait Bounds
You can require multiple traits:
1
2
3
4
5
fn process<T>(value: T)
where
T: Clone + std::fmt::Debug + Send,
{
}
This means T must implement:
1
2
3
Clone
Debug
Send
You can also write:
1
2
fn process<T: Clone + std::fmt::Debug + Send>(value: T) {
}
Both forms are valid.
24. where Clauses
When bounds become complicated, use where:
1
2
3
4
5
6
fn process<T, U>(x: T, y: U)
where
T: Clone + Send,
U: std::fmt::Debug,
{
}
This is often more readable than:
1
2
3
4
5
fn process<T: Clone + Send, U: std::fmt::Debug>(
x: T,
y: U,
) {
}
A good rule:
Use inline bounds for simple constraints and
wherefor complex ones.
25. Bounds on Structs
You can put bounds directly on a struct declaration:
1
2
3
4
5
6
struct Container<T>
where
T: Clone,
{
value: T,
}
However, it is often better to put bounds on the specific impl or function that needs them.
For example:
1
2
3
4
5
6
7
8
9
10
11
12
struct Container<T> {
value: T,
}
impl<T> Container<T>
where
T: Clone,
{
fn duplicate(&self) -> T {
self.value.clone()
}
}
This keeps the struct usable with any T.
26. Bounds on impl
Very common:
1
2
3
4
5
6
7
8
impl<T> Container<T>
where
T: Clone,
{
fn duplicate(&self) -> T {
self.value.clone()
}
}
The implementation exists only for T types that implement Clone.
27. Conditional Methods
You can use trait bounds to make methods available only when required.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct Container<T> {
value: T,
}
impl<T> Container<T> {
fn value(&self) -> &T {
&self.value
}
}
impl<T> Container<T>
where
T: Clone,
{
fn clone_value(&self) -> T {
self.value.clone()
}
}
Therefore:
1
2
3
4
5
6
7
Container<T>
│
├── value()
│
└── clone_value()
↑
only if T: Clone
28. Generic Trait Implementations
You can implement a trait for every type satisfying a bound:
1
2
3
4
5
6
7
8
9
10
11
12
trait Describe {
fn describe(&self);
}
impl<T> Describe for T
where
T: std::fmt::Debug,
{
fn describe(&self) {
println!("{self:?}");
}
}
This is a blanket implementation.
It says:
Every
Tthat implementsDebugalso gets thisDescribeimplementation.
29. Blanket Implementations
The general syntax is:
1
2
3
4
5
impl<T> Trait for T
where
T: SomeBound,
{
}
For example:
1
2
3
4
5
impl<T> MyTrait for T
where
T: Clone,
{
}
This can be extremely powerful.
It is also subject to Rust’s orphan rules, which prevent arbitrary implementations of foreign traits for foreign types.
30. The Orphan Rule
Suppose both are defined in another crate:
1
2
Trait
Type
You cannot arbitrarily write:
1
2
impl ForeignTrait for ForeignType {
}
This restriction prevents conflicting implementations across crates.
Generally, you need to own at least one of:
1
2
trait
type
This is part of Rust’s coherence rules.
31. Generic Type Parameters Can Be Lifetimes Too
Generics and lifetimes can appear together.
For example:
1
2
3
struct Container<'a, T> {
value: &'a T,
}
Here:
1
2
'a → lifetime parameter
T → type parameter
You can also have multiple lifetimes:
1
2
3
4
struct Pair<'a, 'b, T> {
first: &'a T,
second: &'b T,
}
32. Lifetime Bounds on Generic Types
You can write:
1
2
3
4
5
fn process<'a, T>(value: &'a T)
where
T: 'a,
{
}
Here:
1
T: 'a
means T is valid for at least lifetime 'a.
This matters when T itself could contain references.
33. Generic Functions with Lifetimes
Example:
1
2
3
fn first<'a, T>(items: &'a [T]) -> &'a T {
&items[0]
}
Here:
1
2
'a → lifetime
T → type
The returned reference is tied to the input slice.
34. Generic Struct + Lifetime + Trait Bound
You can combine everything:
1
2
3
4
5
6
struct Container<'a, T>
where
T: Clone,
{
value: &'a T,
}
You can also put the bound on the implementation instead:
1
2
3
4
5
6
7
8
9
10
11
12
struct Container<'a, T> {
value: &'a T,
}
impl<'a, T> Container<'a, T>
where
T: Clone,
{
fn clone_value(&self) -> T {
self.value.clone()
}
}
This demonstrates how Rust’s generic system combines:
1
2
3
4
5
6
7
lifetimes
+
types
+
traits
+
bounds
35. impl Trait
Rust provides another generic-looking syntax:
1
2
3
fn create() -> impl Iterator<Item = i32> {
0..10
}
impl Trait means:
The function returns some concrete type that implements this trait.
The caller does not need to know the concrete type.
36. impl Trait in Function Arguments
You can write:
1
2
3
fn print(value: impl std::fmt::Display) {
println!("{value}");
}
This is roughly equivalent to:
1
2
3
fn print<T: std::fmt::Display>(value: T) {
println!("{value}");
}
Both use static dispatch.
However, the syntax and type-system behavior are not identical in every context.
37. impl Trait in Return Position
Example:
1
2
3
fn numbers() -> impl Iterator<Item = i32> {
0..10
}
This hides the concrete iterator type.
The caller only knows:
1
Iterator<Item = i32>
The compiler still knows the exact concrete type.
This is important:
impl Traitis not the same asdyn Trait.
38. impl Trait vs dyn Trait
Compare:
1
fn foo() -> impl Trait
with:
1
fn foo() -> Box<dyn Trait>
impl Trait generally uses static dispatch.
1
2
3
4
5
impl Trait
↓
compiler knows concrete type
↓
static dispatch
dyn Trait uses dynamic dispatch:
1
2
3
4
5
6
7
dyn Trait
↓
trait object
↓
vtable
↓
dynamic dispatch
39. Generic Function = Static Dispatch
Consider:
1
2
3
fn print<T: std::fmt::Display>(value: T) {
println!("{value}");
}
When called with:
1
2
print(10);
print(3.14);
the compiler can generate specialized versions conceptually like:
1
2
print::<i32>
print::<f64>
This is called monomorphization.
40. Monomorphization
Rust generics normally use monomorphization.
Suppose:
1
2
3
fn print<T: Display>(value: T) {
println!("{value}");
}
and:
1
2
print(10);
print(20.0);
The compiler can generate specialized machine-code versions:
1
2
print<i32>
print<f64>
Conceptually:
1
2
3
4
5
6
7
generic source
│
▼
monomorphization
│
├── T = i32
└── T = f64
This is one reason generic Rust code can be both type-safe and fast.
41. Generic Code Is Not Necessarily Runtime Generic
When you write:
1
2
fn foo<T>(value: T) {
}
you might imagine one runtime function that somehow handles every possible type.
That is not generally how Rust implements it.
Rust normally specializes generic code during compilation.
Therefore the runtime usually operates on concrete machine-code versions.
42. Static Dispatch
With:
1
2
fn process<T: Trait>(value: T) {
}
the compiler knows the concrete type.
The call can therefore be statically dispatched.
Conceptually:
1
2
3
4
5
6
7
process::<TypeA>()
↓
known at compile time
process::<TypeB>()
↓
known at compile time
Advantages include:
- optimization opportunities
- no vtable lookup
- no dynamic dispatch overhead
- easier inlining
The tradeoff is potentially larger binaries due to multiple monomorphized copies.
43. Dynamic Dispatch
With:
1
2
fn process(value: &dyn Trait) {
}
the concrete type is not known by the function body.
Instead Rust uses a trait object and a vtable.
Conceptually:
1
2
3
4
5
6
7
8
9
&dyn Trait
│
├── pointer to data
│
└── pointer to vtable
│
├── method A
├── method B
└── ...
The method is selected at runtime.
44. Generic vs Trait Object
Generic:
1
2
fn process<T: Trait>(value: T) {
}
Trait object:
1
2
fn process(value: &dyn Trait) {
}
Generic:
1
2
3
compile time
static dispatch
monomorphization
Trait object:
1
2
3
runtime
dynamic dispatch
vtable
45. Generic Collections
A normal Vec<T> contains one concrete type:
1
let values: Vec<i32> = vec![1, 2, 3];
You cannot do:
1
2
3
4
5
let values = vec![
1,
"hello",
3.14,
];
because a Vec<T> has one T.
If you need different types, you need an abstraction such as:
1
2
3
4
5
enum Value {
Integer(i32),
Float(f64),
Text(String),
}
or trait objects:
1
Vec<Box<dyn Trait>>
This distinction is important:
Generics do not mean “a collection can contain arbitrary types.”
They mean:
A generic abstraction can be instantiated with different concrete types.
46. Generic Type vs Trait Object
Compare:
1
Vec<Box<dyn Display>>
with:
1
Vec<T>
Vec<T>:
1
one concrete T
Vec<Box<dyn Display>>:
1
2
3
4
5
different concrete types
↓
all implement Display
↓
trait objects
This is a major difference between static and dynamic polymorphism.
47. Generic Associated Types
Rust also supports generic associated types (GATs).
For example:
1
2
3
4
5
6
7
trait Iterable {
type Item<'a>
where
Self: 'a;
fn item(&self) -> Self::Item<'_>;
}
Here:
1
Item<'a>
is an associated type that itself has a lifetime parameter.
GATs are particularly useful for abstractions involving borrowing.
48. Generic Associated Types with Types
Associated types can also have type parameters in supported contexts.
Conceptually:
1
2
3
trait Factory {
type Output<T>;
}
The associated type itself becomes parameterized.
GATs are especially common when building advanced iterators, parsers, and borrowing abstractions.
49. Const Generics
Generics are not limited to types.
Rust also supports const generics.
Example:
1
2
3
struct Array<T, const N: usize> {
data: [T; N],
}
Here:
1
2
T → type parameter
N → const parameter
Usage:
1
2
let a: Array<i32, 10>;
let b: Array<f64, 20>;
Conceptually:
1
2
Array<i32, 10>
Array<f64, 20>
50. Why Const Generics Are Useful
Before const generics, expressing an array type with a generic length was much more difficult.
Now we can write:
1
2
3
fn sum<const N: usize>(values: [i32; N]) -> i32 {
values.iter().sum()
}
The same function works with:
1
2
sum([1, 2, 3]);
sum([1, 2, 3, 4, 5]);
The array length becomes part of the type.
51. Const Generic Syntax
The basic syntax is:
1
<const N: usize>
For example:
1
2
fn foo<const N: usize>() {
}
Multiple const parameters:
1
2
fn foo<const N: usize, const M: usize>() {
}
Combined with types:
1
2
fn foo<T, const N: usize>(value: [T; N]) {
}
52. Const Generic Expressions
You may encounter expressions involving const parameters:
1
2
3
struct Buffer<T, const N: usize> {
data: [T; N],
}
The exact set of allowed generic const expressions depends on Rust’s current const-generic capabilities.
Simple uses such as array lengths are common and stable.
53. Generic Parameters Can Include Lifetimes, Types, and Consts
A generic declaration can contain all three:
1
2
3
struct Buffer<'a, T, const N: usize> {
data: &'a [T; N],
}
Here:
1
2
3
'a → lifetime parameter
T → type parameter
N → const parameter
This is the complete conceptual family:
1
2
3
4
Generic parameters
├── lifetimes
├── types
└── const values
54. Parameter Ordering
A common generic declaration is:
1
2
struct Foo<'a, T, const N: usize> {
}
Generally, Rust syntax places:
1
2
3
4
5
lifetimes
↓
type parameters
↓
const parameters
For example:
1
2
3
4
5
fn foo<'a, T, U, const N: usize>(
value: &'a [T; N],
other: U,
) {
}
55. Generic Type Defaults
Generic parameters can have default types in certain declarations.
For example:
1
2
3
struct Container<T = String> {
value: T,
}
Then:
1
2
3
let value = Container {
value: String::from("hello"),
};
can use the default type when inference or context permits.
Defaults are more commonly encountered in library APIs and standard-library types.
56. Default Generic Type Parameters
A well-known example is:
1
2
3
4
struct Point<T = i32> {
x: T,
y: T,
}
Then:
1
2
3
4
Point {
x: 10,
y: 20,
}
can use:
1
T = i32
by default.
But:
1
2
3
4
Point::<f64> {
x: 1.0,
y: 2.0,
}
can explicitly choose another type.
57. Generic Type Parameters in enum
Example:
1
2
3
4
enum Either<A, B> {
Left(A),
Right(B),
}
Usage:
1
2
3
4
let a: Either<i32, String> = Either::Left(10);
let b: Either<i32, String> =
Either::Right(String::from("hello"));
The two variants can carry different types.
58. Generic Functions with Multiple Type Parameters
Example:
1
2
3
fn make_pair<T, U>(first: T, second: U) -> (T, U) {
(first, second)
}
Usage:
1
let pair = make_pair(10, "hello");
The compiler infers:
1
2
T = i32
U = &str
59. Generic Bounds Between Multiple Types
You can constrain relationships between generic types.
For example:
1
2
3
4
5
6
fn compare<T, U>(a: T, b: U)
where
T: PartialEq<U>,
{
println!("{}", a == b);
}
This says:
1
T can be compared with U
The generic relationship is not necessarily:
1
T == U
Instead:
1
T: PartialEq<U>
means T knows how to compare itself with U.
60. Associated Type Constraints
You can constrain an associated type:
1
2
3
4
5
6
fn sum<I>(iter: I) -> i32
where
I: Iterator<Item = i32>,
{
iter.sum()
}
Here:
1
I: Iterator
is the trait bound.
And:
1
Item = i32
constrains the associated type.
61. Fully Qualified Syntax
Generics become especially interesting when traits have associated functions or methods.
For example:
1
2
3
4
5
6
7
8
9
10
11
trait Animal {
fn name() -> &'static str;
}
struct Dog;
impl Animal for Dog {
fn name() -> &'static str {
"Dog"
}
}
You can call:
1
Dog::name();
or use fully qualified syntax:
1
<Type as Trait>::method()
For example:
1
<DOG as Animal>::name()
This syntax is useful when multiple traits provide methods with the same name.
62. Generic Type Syntax with Paths
Generic arguments can appear in type paths:
1
2
3
4
5
Vec<i32>
Option<String>
Result<T, E>
Box<dyn Trait>
Arc<T>
Nested generics:
1
Vec<Option<String>>
More complex:
1
HashMap<String, Vec<i32>>
63. Nested Generic Types
Generics compose naturally.
For example:
1
Vec<Option<Result<String, Error>>>
Read from the inside out:
1
2
3
4
5
6
7
String
↓
Result<String, Error>
↓
Option<Result<String, Error>>
↓
Vec<Option<Result<String, Error>>>
This is one of the reasons generic abstractions are powerful.
64. Generic Type Parameters and Ownership
A generic parameter can represent an owned value:
1
2
fn take<T>(value: T) {
}
When called:
1
2
3
let s = String::from("hello");
take(s);
ownership of s moves into T.
The generic parameter does not change Rust’s ownership rules.
It simply allows the function to work with different concrete types.
65. Generic References
You can borrow a generic type:
1
2
fn inspect<T>(value: &T) {
}
This means:
1
&T
for any T.
With a lifetime:
1
2
fn inspect<'a, T>(value: &'a T) {
}
Now:
1
2
'a → reference lifetime
T → referenced type
66. Generic Mutable References
You can also write:
1
2
fn modify<T>(value: &mut T) {
}
Or explicitly:
1
2
fn modify<'a, T>(value: &'a mut T) {
}
Again:
1
2
'a → lifetime
T → type
67. Generic Ownership vs Borrowing
Compare:
1
2
fn consume<T>(value: T) {
}
with:
1
2
fn borrow<T>(value: &T) {
}
The first takes ownership:
1
2
3
4
5
caller
│
└── moves value
↓
function
The second borrows:
1
2
3
4
5
6
7
caller
│
└── owns value
│
└── temporary reference
↓
function
Generics do not change this behavior.
68. Generic Trait Bounds and Ownership
You can combine ownership and trait bounds:
1
2
3
4
5
fn process<T>(value: T)
where
T: Send + Sync + 'static,
{
}
This is common in concurrent Rust.
The generic parameter T is constrained by several properties.
69. Send and Sync as Generic Bounds
For example:
1
2
3
4
5
6
fn spawn<T>(value: T)
where
T: Send + 'static,
{
// ...
}
This expresses:
1
2
3
T can be transferred to another thread
+
T does not contain short-lived borrowed references
This is why generic APIs can encode concurrency requirements directly into the type system.
70. Sized
One important implicit bound is:
1
T: Sized
By default, generic type parameters are assumed to be Sized.
Conceptually:
1
2
fn foo<T>(value: T) {
}
means approximately:
1
2
fn foo<T: Sized>(value: T) {
}
unless you explicitly opt out.
71. ?Sized
To allow dynamically sized types, use:
1
T: ?Sized
Example:
1
2
3
fn print<T: ?Sized + std::fmt::Display>(value: &T) {
println!("{value}");
}
?Sized means:
Tdoes not have to beSized.
This allows types such as:
1
2
str
dyn Trait
which are dynamically sized types.
72. Why str Needs ?Sized
str is a dynamically sized type.
You cannot normally have:
1
let x: str;
But you can have:
1
let x: &str;
A generic function taking &T can therefore be written:
1
2
fn foo<T: ?Sized>(value: &T) {
}
Now T can be:
1
2
3
Sized type
or
unsized type
73. Sized and ?Sized
Remember:
1
T
implicitly means:
1
T: Sized
while:
1
T: ?Sized
means:
1
T may or may not be Sized
This is particularly important for:
1
2
3
str
dyn Trait
[T]
74. Trait Bounds with ?Sized
You can combine them:
1
2
fn foo<T: ?Sized + Trait>(value: &T) {
}
or:
1
2
3
4
5
fn foo<T>(value: &T)
where
T: ?Sized + Trait,
{
}
75. impl Trait and ?Sized
You will often encounter:
1
&impl Trait
or:
1
Box<dyn Trait>
The important distinction remains:
1
2
3
4
5
6
7
8
9
10
11
generic type parameter
↓
T
opaque return/argument type
↓
impl Trait
trait object
↓
dyn Trait
76. Generic Closures
Closures themselves have compiler-generated anonymous types.
For example:
1
let add = |a: i32, b: i32| a + b;
The closure has a unique compiler-generated type.
You usually interact with it through traits such as:
1
2
3
Fn
FnMut
FnOnce
A generic function can accept a closure:
1
2
3
4
5
6
fn apply<F>(f: F)
where
F: Fn(i32) -> i32,
{
println!("{}", f(10));
}
77. Generic Functions Accepting Closures
Example:
1
2
3
4
5
6
fn apply<F>(function: F, value: i32) -> i32
where
F: Fn(i32) -> i32,
{
function(value)
}
Usage:
1
let result = apply(|x| x * 2, 10);
Here:
1
2
3
F
│
└── closure type
The compiler knows the concrete closure type.
Therefore this is normally statically dispatched.
78. Higher-Ranked Trait Bounds
Advanced generic code may use:
1
for<'a>
For example:
1
F: for<'a> Fn(&'a str) -> &'a str
This means:
For every lifetime
'a,Fcan accept a&'a strand return a&'a str.
This is useful when writing generic code over borrowing closures and functions.
79. Generic Associated Types
GAT syntax can look like:
1
2
3
4
5
6
7
trait Iterable {
type Item<'a>
where
Self: 'a;
fn item(&self) -> Self::Item<'_>;
}
This combines:
1
2
3
4
5
associated types
+
generic parameters
+
lifetimes
GATs are especially useful for APIs that return values borrowing from self.
80. Generic Trait Objects
Trait objects can contain generic associated types and associated type constraints where supported.
For example:
1
Box<dyn Iterator<Item = i32>>
Here:
1
2
3
4
5
Iterator
↓
associated type Item
↓
Item = i32
The trait object itself hides the concrete iterator type.
81. Generic Return Types
A function can return a generic type:
1
2
3
fn make<T>(value: T) -> T {
value
}
The caller determines T.
But a function cannot normally say:
1
2
3
fn make<T>() -> T {
// ...
}
without having some way to determine or construct the required T.
For example:
1
2
3
fn make<T: Default>() -> T {
T::default()
}
Now the trait bound gives the function a way to create T.
82. Generic Associated Functions
A type can have a generic associated function:
1
2
3
4
5
6
7
struct Factory;
impl Factory {
fn create<T: Default>() -> T {
T::default()
}
}
Usage:
1
let x: i32 = Factory::create();
or:
1
let x = Factory::create::<i32>();
83. Generic Methods with Turbofish
For:
1
2
3
4
5
impl Factory {
fn create<T: Default>() -> T {
T::default()
}
}
you can write:
1
Factory::create::<i32>()
The ::<i32> specifies the generic parameter.
84. Generic Constructors
Rust does not have constructors as a special language feature.
But you can create generic constructor-like associated functions:
1
2
3
4
5
6
7
8
9
struct Container<T> {
value: T,
}
impl<T> Container<T> {
fn new(value: T) -> Self {
Self { value }
}
}
Usage:
1
2
let x = Container::new(10);
let y = Container::new(String::from("hello"));
85. Self with Generics
Inside an implementation:
1
2
3
4
5
impl<T> Container<T> {
fn new(value: T) -> Self {
Self { value }
}
}
Self means:
1
Container<T>
So:
1
fn new(value: T) -> Self
is effectively:
1
fn new(value: T) -> Container<T>
86. Generic Type Aliases
You can define:
1
type Pair<T> = (T, T);
Then:
1
let point: Pair<i32> = (10, 20);
You can combine this with lifetimes:
1
type StrRef<'a> = &'a str;
Or const generics:
1
type Array<T, const N: usize> = [T; N];
87. Generic Newtype Pattern
You can wrap a type:
1
struct Wrapper<T>(T);
Usage:
1
2
let x = Wrapper(10);
let y = Wrapper(String::from("hello"));
Conceptually:
1
2
Wrapper<i32>
Wrapper<String>
Newtypes are useful for:
- type safety
- encapsulation
- implementing traits
- distinguishing otherwise identical types
88. Generic Recursive Types
You can create recursive generic structures:
1
2
3
4
enum List<T> {
Cons(T, Box<List<T>>),
Nil,
}
Now:
1
2
List<i32>
List<String>
are possible.
The Box is necessary because recursive types need a known size.
89. Generics and Box
Consider:
1
Box<T>
Box is itself generic.
Conceptually:
1
2
3
4
5
Box<T>
│
├── Box<i32>
├── Box<String>
└── Box<MyType>
The same abstraction works with many types.
90. Generics and Vec
Likewise:
1
Vec<T>
is generic.
Examples:
1
2
3
4
Vec<i32>
Vec<String>
Vec<MyStruct>
Vec<Box<dyn Trait>>
The vector stores one concrete T.
91. Generic APIs in the Standard Library
Rust’s standard library is full of generics:
1
2
3
4
5
6
7
8
9
10
11
12
Option<T>
Result<T, E>
Vec<T>
Box<T>
Rc<T>
Arc<T>
Cell<T>
RefCell<T>
Mutex<T>
RwLock<T>
HashMap<K, V>
HashSet<T>
Understanding generics is therefore essential for reading Rust code.
92. Generics and RefCell
For example:
1
RefCell<T>
works for any T.
1
2
let value = RefCell::new(10);
let text = RefCell::new(String::from("hello"));
Conceptually:
1
2
RefCell<i32>
RefCell<String>
The borrowing behavior is implemented once, generically.
93. Generics and Arc
Similarly:
1
Arc<T>
can contain many types:
1
2
3
Arc<String>
Arc<Vec<u8>>
Arc<MyStruct>
The Arc implementation does not need to be rewritten for each type.
94. Generics and Smart Pointers
A useful mental model:
1
2
3
4
5
6
7
Box<T>
Rc<T>
Arc<T>
Cell<T>
RefCell<T>
Mutex<T>
RwLock<T>
all follow the pattern:
1
Wrapper<T>
where T represents the value being wrapped.
This is one of the most common uses of generics in Rust.
95. Generic Trait Bounds Define Capabilities
Consider:
1
2
3
4
5
fn save<T>(value: T)
where
T: Serialize + Send + Sync,
{
}
The generic parameter says:
1
T can vary
The bounds say:
1
T must have these capabilities
This is a useful way to think about Rust generics:
1
2
3
4
5
6
7
Generic
↓
what can vary
Trait bound
↓
what must be possible
96. Generics and Polymorphism
Rust supports polymorphism primarily through:
Parametric polymorphism
Generics:
1
fn foo<T>(value: T) {}
Ad-hoc polymorphism
Traits:
1
2
trait Foo {
}
Dynamic polymorphism
Trait objects:
1
dyn Foo
These mechanisms solve different problems.
97. Generic Polymorphism
For:
1
2
3
fn print<T: Display>(value: T) {
println!("{value}");
}
the same source code works with many types.
This is parametric polymorphism constrained by a trait.
For example:
1
2
3
T = i32
T = f64
T = String
as long as each type implements Display.
98. Generics vs Inheritance
Rust does not use class inheritance for generic reuse.
Instead, Rust commonly combines:
1
2
3
4
5
generics
+
traits
+
composition
For example:
1
2
fn process<T: Storage>(storage: T) {
}
The function does not care which concrete storage implementation it receives.
This provides polymorphism without requiring an inheritance hierarchy.
99. Generics and Composition
Instead of:
1
2
3
4
5
BaseClass
↑
DerivedClass
↑
MoreDerivedClass
Rust commonly uses:
1
2
3
struct Service<S> {
storage: S,
}
with:
1
2
trait Storage {
}
Then:
1
2
3
Service<FileStorage>
Service<DatabaseStorage>
Service<MemoryStorage>
This is a powerful design pattern in Rust.
100. Generic Dependency Injection
A common pattern is:
1
2
3
4
5
6
7
8
9
10
11
12
13
trait Storage {
fn get(&self, key: &str);
}
struct Service<S> {
storage: S,
}
impl<S: Storage> Service<S> {
fn new(storage: S) -> Self {
Self { storage }
}
}
Now:
1
let service = Service::new(MyStorage);
The service is generic over its storage implementation.
This gives static dispatch and compile-time checking.
101. Generic State Machines
Generics can encode state in the type system.
For example:
1
2
3
4
5
6
struct Connection<State> {
state: State,
}
struct Connected;
struct Disconnected;
Then methods can be implemented only for certain states:
1
2
3
4
5
6
7
impl Connection<Disconnected> {
fn connect(self) -> Connection<Connected> {
Connection {
state: Connected,
}
}
}
This allows invalid state transitions to become compile-time errors.
102. Phantom Types
Sometimes a generic parameter is used only at the type level.
For example:
1
2
3
4
5
6
use std::marker::PhantomData;
struct Id<T> {
value: u64,
_marker: PhantomData<T>,
}
Now:
1
2
3
4
5
struct User;
struct Device;
type UserId = Id<User>;
type DeviceId = Id<Device>;
Even though both contain a u64, the types are different:
1
2
3
UserId
≠
DeviceId
This is useful for type-level safety.
103. PhantomData and Generics
PhantomData<T> tells the compiler that a type logically involves T even though it does not physically store a T.
Example:
1
2
3
4
struct Token<T> {
id: u64,
_marker: PhantomData<T>,
}
The generic parameter participates in type checking without requiring an actual T field.
This can also affect:
- variance
- ownership reasoning
- auto traits
- drop checking
104. Generic Associated Constants
Traits and types can also have associated constants that work with generic types.
For example:
1
2
3
trait Limits {
const MAX: usize;
}
A generic type can implement the trait:
1
2
3
impl<T> Limits for Vec<T> {
const MAX: usize = 1024;
}
The implementation applies to every Vec<T>.
105. Generic Drop
You can implement Drop for a generic type:
1
2
3
4
5
6
7
8
9
struct Container<T> {
value: T,
}
impl<T> Drop for Container<T> {
fn drop(&mut self) {
println!("dropping");
}
}
The implementation applies to every Container<T>.
106. Generic Trait Bounds on Associated Types
You can combine bounds:
1
2
3
4
5
6
7
fn process<I>(iter: I)
where
I: Iterator,
I::Item: std::fmt::Debug,
{
// ...
}
Here:
1
2
3
I implements Iterator
+
I::Item implements Debug
This pattern is very common in generic Rust code.
107. Nested where Constraints
Complex APIs may look like:
1
2
3
4
5
6
7
fn process<T, U>(value: T, other: U)
where
T: Clone + Send,
U: Iterator,
U::Item: std::fmt::Debug,
{
}
Read the bounds one at a time.
Do not try to understand the whole signature at once.
108. Generic Function with where + Lifetime
Example:
1
2
3
4
5
6
fn process<'a, T>(value: &'a T)
where
T: 'a + std::fmt::Debug,
{
println!("{value:?}");
}
Break it down:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
'a
↓
lifetime
T
↓
generic type
T: 'a
↓
T is valid for 'a
T: Debug
↓
T supports Debug
109. Generic Function with Lifetime + Trait Object
You may encounter:
1
2
fn process<'a>(value: &'a dyn Trait) {
}
This means:
1
2
3
4
5
6
7
'a
↓
lifetime of the reference
dyn Trait
↓
trait object
Compare:
1
fn process<T: Trait>(value: &T)
with:
1
fn process(value: &dyn Trait)
The first is generic/static dispatch.
The second is dynamic dispatch.
110. Generic Function Returning Trait Objects
You can write:
1
2
3
fn create() -> Box<dyn Trait> {
Box::new(MyType)
}
The caller does not know the concrete type.
Compare with:
1
2
3
fn create() -> impl Trait {
MyType
}
Both hide the concrete return type, but they use different mechanisms.
111. Generic Function Returning impl Trait
Example:
1
2
3
fn numbers() -> impl Iterator<Item = i32> {
0..10
}
The compiler knows the concrete type.
The caller only knows the trait interface.
This is an opaque type.
112. Generic Function Accepting impl Trait
Example:
1
2
3
fn process(value: impl Display) {
println!("{value}");
}
This is approximately equivalent to:
1
2
3
fn process<T: Display>(value: T) {
println!("{value}");
}
Use whichever syntax makes the API clearer.
113. Generic Type Parameter vs impl Trait
Compare:
1
fn foo<T: Display>(x: T) -> T
and:
1
fn foo(x: impl Display) -> impl Display
The first explicitly names T, so you can use the same type parameter elsewhere:
1
fn foo<T: Display>(x: T, y: T) -> T
With:
1
fn foo(x: impl Display, y: impl Display)
the two parameters may have different concrete types.
This is an important difference.
114. impl Trait Parameters Can Represent Different Types
For:
1
2
fn foo(x: impl Display, y: impl Display) {
}
conceptually:
1
2
3
4
5
x: T
y: U
T: Display
U: Display
So:
1
foo(10, "hello");
can work.
But:
1
2
fn foo<T: Display>(x: T, y: T) {
}
requires:
1
x and y have the same T
115. Generic Parameters Express Equality of Types
For:
1
2
3
fn pair<T>(x: T, y: T) -> (T, T) {
(x, y)
}
both arguments must have the same type.
But:
1
2
fn pair(x: impl Display, y: impl Display) {
}
does not require the same type.
This makes generic syntax semantically important.
116. Generic Return Types Must Be Determined
Consider:
1
2
3
fn create<T>() -> T {
// ?
}
The compiler cannot magically create an arbitrary T.
A trait bound can provide the necessary operation:
1
2
3
fn create<T: Default>() -> T {
T::default()
}
This is a common generic design pattern.
117. Generic Bounds as Interfaces
A generic function:
1
2
fn save<T: Write>(value: T) {
}
does not care whether T is:
1
2
3
4
File
TcpStream
Vec<u8>
CustomWriter
It only requires:
1
T: Write
This is similar to programming against an interface.
But unlike inheritance-based interfaces, Rust traits are independent capabilities that types can implement.
118. Static Dispatch and Performance
Generic code:
1
2
fn process<T: Trait>(value: T) {
}
can often be optimized aggressively.
Because the compiler knows:
1
2
exact type
exact implementation
it can potentially:
- inline calls
- remove abstraction overhead
- specialize code
- optimize data layout
This is one of the major strengths of Rust’s generics.
119. The Cost of Monomorphization
Monomorphization is not free.
If a generic function is instantiated for many different types:
1
2
3
4
5
T = A
T = B
T = C
T = D
...
the binary may contain multiple specialized versions.
Potential consequences:
- larger binaries
- longer compilation times
- more generated machine code
This is one reason dynamic dispatch can sometimes be useful.
120. Generics vs Dynamic Dispatch
A useful comparison:
| Feature | Generics | dyn Trait |
|---|---|---|
| Dispatch | Static | Dynamic |
| Concrete type known by compiler | Yes | No |
| Monomorphization | Yes | No |
| Vtable | No | Yes |
| Runtime dispatch cost | Usually none | Usually small |
| Binary size | Can increase | Often smaller |
| Heterogeneous collection | Not directly | Yes |
| Compile-time optimization | Strong | More limited |
Neither approach is universally better.
Choose based on the API and requirements.
121. Generic Code and Zero-Cost Abstractions
Rust often aims for abstractions that compile down to code comparable to handwritten specialized code.
For example:
1
2
3
fn process<T: Trait>(value: T) {
value.method();
}
The generic abstraction itself does not necessarily introduce runtime overhead.
The compiler can specialize it.
This is one of the reasons generics are heavily used throughout Rust’s standard library.
122. Generics and Type Safety
Generics prevent many classes of mistakes.
For example:
1
2
struct UserId(u64);
struct DeviceId(u64);
Even though both contain:
1
u64
they are different types.
Generic wrappers can extend this idea:
1
2
3
4
struct Id<T> {
value: u64,
_marker: PhantomData<T>,
}
Now:
1
2
Id<User>
Id<Device>
cannot accidentally be mixed.
123. Generics and API Design
When designing a generic API, ask:
What varies?
Use a generic parameter:
1
<T>
What must the type be able to do?
Use a trait bound:
1
T: Trait
Does the API need to return an abstract implementation?
Consider:
1
impl Trait
Does the API need heterogeneous values?
Consider:
1
dyn Trait
Does the type need to encode a number?
Consider:
1
const N: usize
Does the type borrow data?
Consider:
1
'a
124. Generic Syntax Cheat Sheet
One type parameter
1
<T>
Multiple type parameters
1
<T, U>
Generic function
1
2
fn foo<T>(value: T) {
}
Generic function with return type
1
2
3
fn foo<T>(value: T) -> T {
value
}
Generic struct
1
2
3
struct Foo<T> {
value: T,
}
Generic enum
1
2
3
enum Foo<T> {
Value(T),
}
Generic trait
1
2
trait Foo<T> {
}
Generic implementation
1
2
impl<T> Foo<T> {
}
Generic trait implementation
1
2
impl<T> Trait for Foo<T> {
}
Trait bound
1
T: Trait
Multiple bounds
1
T: TraitA + TraitB
where
1
2
3
4
5
6
fn foo<T, U>(x: T, y: U)
where
T: TraitA,
U: TraitB,
{
}
Associated type constraint
1
T: Iterator<Item = i32>
impl Trait parameter
1
2
fn foo(value: impl Trait) {
}
impl Trait return
1
2
fn foo() -> impl Trait {
}
Trait object
1
dyn Trait
Boxed trait object
1
Box<dyn Trait>
Lifetime + generic type
1
2
fn foo<'a, T>(value: &'a T) {
}
Lifetime bound
1
T: 'a
Lifetime outlives
1
'a: 'b
Anonymous lifetime
1
'_
Higher-ranked trait bound
1
for<'a>
Sized
1
T: Sized
Allow unsized types
1
T: ?Sized
Const generic
1
<const N: usize>
Type + const generic
1
<T, const N: usize>
Lifetime + type + const
1
<'a, T, const N: usize>
Turbofish
1
::<T>
Multiple turbofish arguments
1
::<T, U>
125. A Complete Generic Example
Here is an example combining many of the concepts:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
use std::fmt::Debug;
struct Container<'a, T, const N: usize> {
values: &'a [T; N],
}
impl<'a, T, const N: usize> Container<'a, T, N>
where
T: Debug,
{
fn print(&self) {
println!("{:?}", self.values);
}
fn first(&self) -> &T {
&self.values[0]
}
}
This contains:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
'a
lifetime parameter
T
type parameter
N
const parameter
T: Debug
trait bound
&'a [T; N]
borrowed array
impl<'a, T, const N: usize>
generic implementation
This is a good example of how Rust’s generic system combines multiple concepts.
126. How to Read Complex Generic Signatures
When you see a complicated Rust signature, don’t try to understand it all at once.
For example:
1
2
3
4
5
6
7
8
9
10
fn process<'a, T, U, const N: usize>(
value: &'a [T; N],
other: U,
) -> &'a T
where
T: Clone + Debug,
U: Send + 'static,
{
&value[0]
}
Read it in layers.
Layer 1: Lifetime
1
'a
There is a lifetime parameter.
Layer 2: Types
1
2
T
U
There are two type parameters.
Layer 3: Const
1
const N: usize
There is a compile-time integer parameter.
Layer 4: Inputs
1
&'a [T; N]
A reference to an array of N values of type T.
1
U
An owned value of type U.
Layer 5: Output
1
&'a T
A reference to T tied to 'a.
Layer 6: Bounds
1
T: Clone + Debug
T must implement Clone and Debug.
1
U: Send + 'static
U must satisfy Send and 'static.
This layered approach makes complex Rust signatures much easier to understand.
127. Generics Are Compile-Time Abstraction
A useful mental model is:
1
2
3
4
5
6
7
8
9
10
11
Generic source code
│
▼
Compiler
│
├── T = i32
├── T = String
└── T = MyType
│
▼
Concrete machine code
The generic abstraction exists primarily at compile time.
This is different from dynamic dispatch, where the runtime may need to determine which implementation to call.
128. The Three Main Generic Parameter Categories
Rust’s generic parameters can be understood as three categories:
1
2
3
4
5
6
7
8
9
10
Generic parameters
│
├── Lifetime
│ 'a
│
├── Type
│ T
│
└── Const
N
For example:
1
2
3
struct Buffer<'a, T, const N: usize> {
data: &'a [T; N],
}
contains all three.
129. Generics, Traits, and Lifetimes Work Together
These three concepts are often learned separately, but real Rust code combines them.
For example:
1
2
3
4
5
fn process<'a, T>(value: &'a T)
where
T: Clone + Send + 'a,
{
}
Here:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
'a
lifetime
T
generic type
Clone
capability
Send
concurrency capability
T: 'a
lifetime relationship
Rust’s type system allows all these constraints to be expressed together.
130. The Big Picture
Generics answer:
What type can vary?
Traits answer:
What behavior does that type provide?
Trait bounds answer:
What behavior must the generic type provide?
Lifetimes answer:
How long are borrowed references valid?
Const generics answer:
What compile-time value can vary?
impl Trait answers:
Can I hide the concrete type while exposing its capabilities?
dyn Trait answers:
Can I use runtime polymorphism through a trait object?
Monomorphization answers:
How can generic code become concrete machine code?
131. Final Mental Model
When you see:
1
fn process<T: Trait>(value: T)
think:
1
2
3
4
5
T
│
└── some concrete type
│
└── must implement Trait
When you see:
1
fn process<'a, T>(value: &'a T)
think:
1
2
3
4
5
6
7
'a
│
└── lifetime of the reference
T
│
└── type being referenced
When you see:
1
fn process<T, U>(x: T, y: U)
think:
1
T and U may be different types
When you see:
1
fn process<T>(x: T, y: T)
think:
1
x and y must have the same type T
When you see:
1
fn process(x: impl Trait)
think:
1
some concrete type implementing Trait
When you see:
1
fn process(x: &dyn Trait)
think:
1
2
3
trait object
+
dynamic dispatch
When you see:
1
T: ?Sized
think:
1
T may be dynamically sized
When you see:
1
<const N: usize>
think:
1
N is a compile-time value
When you see:
1
for<'a>
think:
1
for every possible lifetime 'a
And the most important idea is:
Generics allow Rust to abstract over types, lifetimes, and compile-time values while preserving strong compile-time type checking and, in the common case, enabling static dispatch and zero-cost abstractions.