Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

Bug

#![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 implement Float (it must behave like a floating point number: support powi, abs, etc.)
  • F must implement Sum (so we can sum an iterator of F)

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 of F.

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.