Calling generic function in impl requires trait bound

I am fairly new to rust and am wondering why I need the add a trait bound related to a function called within methods. It feels like spooky-action-at-a-distance and is disconcerting.

I did a little experiment to simulate from an ARMA(P,Q) model. A portion of the code looks like:

pub fn sample_normal<T, R: Rng>(mean: T, sd: T, rng: &mut R) -> T
where
    T: Add<Output = T> + Mul<Output = T>,
    StandardNormal: Distribution<T>,
{
    rng.sample::<T, StandardNormal>(StandardNormal) * sd + mean
}

pub struct AR<const P: usize, T> {
    state: ArrayDeque<T, P, Wrapping>,
    pars: [T; P],
    mean: T,
    stdev: T,
}

impl<const P: usize, T> AR<P, T>
where
    T: Zero + Copy + Add<Output = T> + Mul<Output = T>,
    for<'a> &'a T: Mul<&'a T, Output = T>,
    StandardNormal: Distribution<T>, // Why???
{
    pub fn new(state: [T; P], pars: [T; P], mean: T, stdev: T) -> Self {
        Self {
            state: state.into_iter().collect(),
            pars,
            mean,
            stdev,
        }
    }
    pub fn randinit<R: Rng>(pars: [T; P], mean: T, stdev: T, rng: &mut R) -> Self {
        let mut res = Self {
            state: std::iter::repeat_with(|| sample_normal(Zero::zero(), stdev, rng))
                .take(P)
                .collect(),
            pars,
            mean,
            stdev,
        };
        for _ in 0..P * P * 10 {
            res.next(rng);
        }
        res
    }
    fn update_state(&mut self, state: T) {
        self.state.push_front(state);
    }
    fn project(&self) -> T {
        self.state
            .iter()
            .zip(self.pars.iter())
            .fold(Zero::zero(), |sum, x| sum + x.0 * x.1)
    }
    pub fn next<R: Rng>(&mut self, rng: &mut R) -> T {
        let noise = sample_normal(Zero::zero(), self.stdev, rng);
        let next = self.project() + noise;
        self.update_state(next);
        self.mean + next
    }
    pub fn gen<R: Rng>(&mut self, n: usize, rng: &mut R) -> Vec<T> {
        std::iter::repeat_with(|| self.next(rng)).take(n).collect()
    }
}

It seems the trait bound is required in any scope calling sample_normal. I have also seen this where wrapping a container-like object required a trait bound like Allocator: DefaultAllocator<T>. I don't see why it is not implicit, especially because when working with other crates, these trait bound are often not documented and it requires a lot of guesswork to figure out. At least that's been my experience while stumbling through generics. Isn't it enough to give the trait bound in the function definition? (Edit: I should mention that in my code, sample_normal is in a different module.)

Just to make sure we're on the same page first,

  • You're calling this method that requires D: Distribution<T>
  • You're calling it with D = StandardNormal
  • StandardNormal only implements Distribution<T> for T = f32 and T = f64
  • So really this is a restriction on T, it just happens to be on the right side of the trait bound
    • The bounds are constraints to be satisfied; you can have x < 10 or 10 > x for a variable x

With that context, the reason that you have to state the bound is that function APIs are contracts that the compiler enforces on both sides. Callers have to meet the bounds of the function, and the function body cannot use capabilities beyond their bounds.

So as a caller, you can't call rng::sample<T, StandardNormal> unless StandardNormal: Distribution<T>; they've put that bound on their function and you must meet it.

And as a function body writer, you can't assume StandardNormal: Distribution<T> unless you've declare the bound on your function that StandardNormal: Distribution<T>; if you need that capability, you have to let callers of your function know it. And in turn they'll be forced to meet it.


If the question is "why does the language force me to add the bounds instead of inferring it from the function body", it's for a number of reasons:

  • If it didn't, you could break far-away code by changing what should be implementation details in the body of your function
    • I.e. disconcerting action at a distance
  • Consumers of your function would be surprised when there are additional, undeclared constraints on calling the function
    • It's required for you to give the trait bound in your function definition just like everybody else
    • I.e. it is not enough for it to be declared on some function you transitively call in your function body; consumers of your function don't want to have to read your code or otherwise care about your function body
  • The compiler would have to do global reasoning, parsing every function of the entire program call graph, to know if all the pieces work together
    • Method resolution would also trigger a lot more ambiguity errors for generics if they could exploit undeclared bounds
4 Likes

This makes more sense. Having T appear on the right side of the trait bound threw me.

The earlier example I referred to was trying to wrap nalgebra::base::Matrix in a struct. Perhaps not the best place to start, but I don't see Allocator mentioned in the definition here.

While I can see why it might not be the best option, the compiler could in theory defer checking the bound until it specializes sample_normal for T, right?

Edit: On second thought, I see how this is a problem if the source for sample_normal is not available because its in a different compilation unit.

Yes, this is technically possible. Rust's choice to not do this is instead a conscious design decision to require every function signature to carry all of the type information necessary to call it successfully, for the benefit of downstream programmers.

2 Likes

Others have already answered why this is the case. I just want to specifically point out one concrete, incorrect premise in your question:

As it should be obvious from the explanations above, Rust's behavior is the opposite of spooky-action-at-a-distance. Requiring explicit trait bounds means that once a generic function's body was successfully typechecked in itself, it can never fail compilation, no matter what (bounds-meeting) types its type parameters will be instantiated to.

Contrast this with C++ templates, for example, where you won't find out that your template doesn't compile as intended until some 3rd-party stars using it in a way that you didn't perfectly foresee.

It's perfectly normal for type variables to appear on either side of a bound. where clauses and bounds don't "constrain the LHS". They establish a relationship between the LHS and the RHS. Of course, the most common form is TypeVar: Trait. However, this is not the only useful and/or necessary form.

For example, I am developing a database abstraction layer which can work with different back-ends (such as SQLite and Postgres). Not all Rust-native types can be represented (directly) by every database. For example, Postgres has native date/time types, while SQLite does not. The requirement for a DB driver to understand datetimes can be expressed in the code of a query dealing with dates, for example in the following (hypothetical/simplified) way:

fn users_of_age<D>(current_date: DateTime) -> Result<Vec<User>, D::Error>
where
    D: DbDriver,
    DateTime: ToSQL<D>,
{
    // ...query logic here...
}

Clearly, here it is a DateTime that should be "constrained" so that it's expressible by the given DB engine. However, this doesn't really only constrain DateTime, it also constrains D itself, because DateTime doesn't (necessarily) implement ToSQL<D> for every possible D. Yet it's not D itself that bears the trait bound, given that it's not the implementor of the (hypothetical) ToSQL trait – rather, DateTime is.

1 Like

Thanks. Very clear to me now.

I am really looking forward to trait aliases. Imagine a dozen call points, each with a dozen different bounds. That's going to be a long list to type. But, as you point out, much preferable to obviating the requirements and finding out later.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.