In this lesson, you will learn how to design generic functions and structures so that you can add features later without forcing your users to update their existing code.
The Problem of "Overspecified" Bounds
When you write a generic function, you often start by requiring a specific trait. For example, if you are building a data logger, you might require T: Display:
pub fn log_data<T: Display>(item: T) {
println!("{}", item);
}
If you later decide that your logger also needs to support serialization, you might be tempted to change the signature to fn log_data<T: Display + Serialize>(item: T). However, this is a breaking change. Any user who previously passed a type that implemented Display but not Serialize will see their code fail to compile.
Strategies for Stability
To evolve your library without breaking changes, consider these two patterns:
- Introduce New Functions: Instead of modifying the signature of
log_data, introducelog_data_serde<T: Display + Serialize>(item: T). This allows users to opt-in to the new requirements when they are ready. - Default Traits: Use a "marker" trait or a default generic parameter. You can define a trait that acts as a wrapper. If you want to add functionality to a struct, provide a secondary method that requires the new trait bound, rather than forcing it onto existing generic methods.
A Pattern for Evolution
A clean way to maintain compatibility is to use a "sealed" trait or a blanket implementation. You can keep the original function signature stable by delegating to a trait that you can extend internally:
pub trait Loggable {
fn format(&self) -> String;
}
// Keep this stable for years
pub fn log_data<T: Loggable>(item: T) {
println!("{}", item.format());
}
// Add new features via an extension trait
pub trait LoggableExt: Loggable {
fn log_with_timestamp(&self) { /* ... */ }
}
By relying on trait inheritance rather than tightening bounds on core generic functions, you ensure that your library remains a stable foundation for your users, even as your feature set grows.
Key Takeaway: Never tighten trait bounds on existing generic functions; instead, use extension traits or new function names to provide additional functionality while keeping the public API surface compatible.