Trait bounds
Generics aren't very ergonomic in most other languages, if they exist at all. The reason is that generics usually rely on duck typing, meaning you use any function on your generic type and just hope that the type used for instantiating happens to have a function with matching name and signature. That makes it difficult to reason about the code and obviously, auto completion etc only barely works, if at all. This leads to programmers avoiding generic code in most cases, to the point of arguing that "almost no code can be shared between types". That might actually be right for other languages, but in Rust, generics are a core feature that isn't just used extensively, but also way more ergonomic. Enter the happy land that is trait bounds!
When using generics, Rust doesn't allow you to make any assumptions about the type's properties, so you can either only use standard type operations like moving an object around or referencing it, or you can constrain the type with trait bounds and get access to the properties of that trait in return. This works exactly the same way as trait extension does.
With multiple generic parameters, as well as multiple trait bounds per parameter, the signature of your type or function will become quite long and difficult to parse for humans.
Because of this, Rust provides an alternative syntax where instead of declaring the trait bounds directly at the generic parameter declaration, you can put the where
keyword, followed by the trait bounds, directly before opening the block.
trait FirstTrait {
fn first_method(&self);
}
trait SecondTrait {
fn second_method(&self);
}
// Direct bounds
fn example<T: FirstTrait + SecondTrait>(value: T) {
value.first_method();
value.second_method();
}
// Bounds with 'where'
fn example2<T>(value: T) -> bool where T: FirstTrait + SecondTrait {
value.first_method();
value.second_method();
true
}
Trait bounds work just the same when generically implementing a type as trait extensions. As you can have multiple impl
blocks for the same type, you can actually give them different trait bounds, optionally implementing functions depending on the type you use to instantiate it.
Scaling the capability of a type by the capability of its generic parameter is one of the many demonstrations on how flexible the Rust traits and generics really are.
As an alternative of declaring generic parameters before using them, Rust also provides the impl
syntax. By that, I do not mean the impl
blocks we've seen before, but a keyword for function parameters (doesn't work on types) that lets you use trait bounds instead of types for parameters and/or return type.
Any of those bounds represent a generic type that's independent from any other trait bound, even if that's of the same type. But as generics aren't dynamic types and get instantiated at compile time, every code path inside such a function still has to return the same type, even if the signature is generic.
use std::fmt::Display;
// The return value gets reduced to just its bound,
// so this will return an essentially useless type!
fn print_and_clone(print_object: impl Display, clone_object: &impl Clone)
-> impl Clone {
println!("{}", print_object);
clone_object.clone()
}