Closed-form estimator with generic types and trait bounds
What are generics?
Generics let you write code that works with many types, not just one.
Instead of writing:
#![allow(unused)] fn main() { struct RidgeEstimator { beta: f64, } }
You can write:
#![allow(unused)] fn main() { struct RidgeEstimator<F> { beta: F, } }
Here, F
is a type parameter — it could be f32
, f64
, or another type. In Rust, generic types have no behavior by default.
#![allow(unused)] fn main() { fn sum(xs: &[F]) -> F { xs.iter().sum() // This will not compile } }
The compiler gives an error: "F
might not implement Sum
, so I don’t know how to .sum()
over it."
Trait bounds
To fix that, we must tell the compiler which traits F
should implement.
For example:
#![allow(unused)] fn main() { impl<F: Float + Sum> RidgeModel<F> for GenRidgeEstimator<F> { ... } }
This means:
F
must implementFloat
(it must behave like a floating point number: supportpowi
,abs
, etc.)F
must implementSum
(so we can sum an iterator ofF
)
This allows code like:
#![allow(unused)] fn main() { let mean = xs.iter().copied().sum::<F>() / F::from(xs.len()).unwrap(); }
Using generic bounds allows the estimator to work with f32
, f64
, or any numeric type implementing Float
. The compiler can generate specialized code for each concrete type.
Generic Ridge estimator
Our Ridge estimator that works with f32
and f64
takes this form:
#![allow(unused)] fn main() { use num_traits::Float; use std::iter::Sum; /// A trait representing a generic Ridge regression model. /// /// The model must support fitting to training data and predicting new outputs. // ANCHOR: ridge_model_trait pub trait RidgeModel<F: Float + Sum> { /// Fits the model to the given data using Ridge regression. fn fit(&mut self, x: &[F], y: &[F], lambda2: F); /// Predicts output values for a slice of new input features. fn predict(&self, x: &[F]) -> Vec<F>; } // ANCHOR_END: ridge_model_trait /// A generic Ridge regression estimator using a single coefficient `beta`. /// /// This implementation assumes a linear relationship between `x` and `y` /// and performs scalar Ridge regression (1D). // ANCHOR: gen_ridge_estimator pub struct GenRidgeEstimator<F: Float + Sum> { beta: F, } // ANCHOR_END: gen_ridge_estimator // ANCHOR: gen_ridge_estimator_impl impl<F: Float + Sum> GenRidgeEstimator<F> { /// Creates a new estimator with the given initial beta coefficient. pub fn new(init_beta: F) -> Self { Self { beta: init_beta } } } // ANCHOR_END: gen_ridge_estimator_impl // ANCHOR: gen_ridge_estimator_trait_impl impl<F: Float + Sum> RidgeModel<F> for GenRidgeEstimator<F> { /// Fits the Ridge regression model to 1D data using closed-form solution. /// /// This method computes the regression coefficient `beta` by minimizing /// the Ridge-regularized least squares loss. /// /// # Arguments /// - `x`: Input features. /// - `y`: Target values. /// - `lambda2`: The regularization parameter (λ²). fn fit(&mut self, x: &[F], y: &[F], lambda2: F) { let n: usize = x.len(); let n_f: F = F::from(n).unwrap(); assert_eq!(x.len(), y.len(), "x and y must have the same length"); let x_mean: F = x.iter().copied().sum::<F>() / n_f; let y_mean: F = y.iter().copied().sum::<F>() / n_f; let num: F = x .iter() .zip(y.iter()) .map(|(xi, yi)| (*xi - x_mean) * (*yi - y_mean)) .sum::<F>(); let denom: F = x.iter().map(|xi| (*xi - x_mean).powi(2)).sum::<F>() + lambda2 * n_f; self.beta = num / denom; } /// Applies the trained model to input features to generate predictions. /// /// # Arguments /// - `x`: Input features to predict from. /// /// # Returns /// A vector of predicted values, one for each input in `x`. fn predict(&self, x: &[F]) -> Vec<F> { x.iter().map(|xi| *xi * self.beta).collect() } } // ANCHOR_END: gen_ridge_estimator_trait_impl }
Notice that the trait bounds <F: Float + Sum> RidgeModel<F>
are defined after the name of a trait
or struct
, or right next to an impl
.
Summary
- Generics support type-flexible code.
- Trait bounds like
<F: Float + Sum>
constrain what operations are valid. - Without
Sum
, the compiler does not allow.sum()
on iterators ofF
.
Try removing Sum
from the bound:
#![allow(unused)] fn main() { impl<F: Float> RidgeModel<F> for GenRidgeEstimator<F> }
And keep a call to .sum()
. The compiler should complain:
error[E0599]: the method `sum` exists for iterator `std::slice::Iter<'_, F>`,
but its trait bounds were not satisfied
To resolve this, add + Sum
to the bound.