Constrain trait function further than trait definition

I have the following example (which doesn't compile):

struct Point<T, U> {
    x: T,
    y: U,
}

impl<T, U> Point<T, U> 
where 
    T : Copy,
    U : Copy,
{
    fn new(x: T, y: U) -> Self {
        Self { x, y }
    }

    fn x(self: &Self) -> T {
        self.x
    }

    fn y(self: &Self) -> U {
        self.y
    }
}

trait Distance<T> {
    fn distance<V, W>(self: &Self, other: &Point<V, W>) -> T
    where
        V: Copy,
        W: Copy;
}

impl<T, U> Distance<f64> for Point<T, U>
where
    T: Copy + Into<f64>,
    U: Copy + Into<f64>,
{
    fn distance<V, W>(self: &Self, other: &Point<V, W>) -> f64
    where
        V: Copy + Into<f64>,
        W: Copy + Into<f64>,
    {
        // Cast fields to f64 and do all the calculations as f64
    }
}

impl<T, U> Distance<i32> for Point<T, U>
where
    T: Copy + Into<i32>,
    U: Copy + Into<i32>,
{
    fn distance<V, W>(self: &Self, other: &Point<V, W>) -> i32
    where
        V: Copy + Into<i32>,
        W: Copy + Into<i32>,
    {
        // Cast fields to i32 and do all the calculations as i32
    }
}

How can I further constrain the function distance implementation so that V and W are Into, while on the trait definition they remain only Copy, so in the call site I could do:


let p1 = Point::new(10, 10);
let p2 = Point::new(20, 20);
let integral_distance = Distance::<i32>::distance(&p1, &p2);
let float_distance = Distance::<f64>::distance(&p1, &p2);

It seems odd that I can constrain Point<T, U> further while using a trait but I can't constrain further the function of a trait on its implementation, after all, it is more strict on its' implementation than on its' definition. Or maybe I am missing something? ( I am just learning Rust and I come from a C++ background).

This is the minimum change necessary to fix the type errors, and it does not prevent the call sites from using code like you wish to use:

trait Distance<T> {
    fn distance<V, W>(self: &Self, other: &Point<V, W>) -> T
    where
        V: Copy + Into<T>,
        W: Copy + Into<T>;
}

But, I would personally not put the constraints on each method! You can put them on the implementations instead:

trait Distance<T> {
    fn distance<V, W>(self: &Self, other: &Point<V, W>) -> T;
}

impl<T, U> Distance<f64> for Point<T, U>
where
    T: Copy + Into<f64>,
    U: Copy + Into<f64>,
{
    fn distance<V, W>(self: &Self, other: &Point<V, W>) -> f64 {
        // Cast fields to f64 and do all the calculations as f64
        todo!()
    }
}

impl<T, U> Distance<i32> for Point<T, U>
where
    T: Copy + Into<i32>,
    U: Copy + Into<i32>,
{
    fn distance<V, W>(self: &Self, other: &Point<V, W>) -> i32 {
        // Cast fields to i32 and do all the calculations as i32
        todo!()
    }
}

Notice the trait itself is unconstrained. It requires no proof from the type system because the actual conversions are the job of each individual implementation. The implementation can choose for itself whether it wants to do the conversion with Into or TryInto, for instance.

Correct. If you do this, you will get an error saying as much:

error[E0276]: impl has stricter requirements than trait

You don't see this error in the example code because type checking first fails to satisfy the f32: From<V> et al. impls. But if that type error were not encountered, you would see E0276.

It's hard to say, but maybe there is some confusion between the two positions of the where clauses: On impl (which is where generic parameters for the entire implementation are declared and constrained) and on fn (which is where generics for the specific method are declared and constrained). The latter position (fn) applies identical constraints to all implementors.

I prefer to make trait declarations maximally relaxed and implementations minimally constrained.

The problem is exactly that the implementation is more strict than the trait definition.

If the caller of a method satisfies all the bounds in the trait definition, then they can call the method. Implementations are not allowed to require additional bounds.[1]

So consider this use of your OP trait:

fn example<D: Distance<i32>>(this: &D, other: &Point<(), i64>) {
    let d: i32 = this.distance(point);
}

All the bounds from the trait are satisfied:

  • D: Distance<i32>
  • (): Copy
  • i64: Copy

So the caller is allowed to call the method. This must compile, for every possible D where D: Distance<i32> is satisfied. It's not allowed to throw a post-monomorphization error like templates may.[2]

If your impl<T, U> Distance<i32> for Point<T, U> was accepted, this would be a problem,[3] because your method isn't necessarily defined for V = () or W = i64 -- due to your additional bounds.[4]

One fix is to put the bounds in your trait:

trait Distance<T> {
    fn distance<V, W>(self: &Self, other: &Point<V, W>) -> T
    where
        V: Copy + Into<T>,
        W: Copy + Into<T>;
}

It's not the only solution; perhaps a different design would be more useful. This has a feel of being a mix of too-generic with too-concrete. Perhaps distance should take an other: &Self or other: Self[5] or an Other: Into<Self> etc.

I don't follow. Can you supply an example?


By removing the bounds, no implementer can do anything with V, W except what's possible knowing they're Sized.[6] You would need to move the generics up to the trait definition instead of being on the method in order to give the implementor more flexibility... if I understand your suggestion correctly.


  1. They can require less bounds, but callers cannot take advantage of that, except where refinement applies -- a newer concept I'll ignore for this comment. ↩︎

  2. I believe there are some exceptions in the language around higher-ranked bounds, which I'll ignore for this comment. ↩︎

  3. with say D = Point<i32, i32> ↩︎

  4. There is no impl Into<i32> for () or impl Into<i32> for i64 ↩︎

  5. since everything is Copy anyway ↩︎

  6. In the OP they could only do that plus what's possible due to Copy. ↩︎

Hmm, of course, you are right.

What I missed about the code in OP was the differing types self: Point<T, U> and other: Point<V, W>. I thought both arguments were the same! Thanks for pointing that out.

2 Likes