How do I implement generic commutative std::ops in

2020-04-08 14:03发布

问题:

I have:

use std::ops::{Add, Div, Mul, Neg, Sub};

pub trait Hilbert:
    Add + Sub + Mul + Div + Neg + Mul<f64, Output = Self> + Div<f64, Output = Self> + Sized + Copy
{
    fn dot(&self, other: &Self) -> f64;
    fn magnitude(&self) -> f64;
}

fn g<T: Hilbert>(x: T) -> f64 {
    let a = (x * 2.0).dot(&x);
    let b = (2.0 * x).dot(&x);
    a + b
}
error[E0277]: cannot multiply `T` to `{float}`
  --> src/main.rs:12:18
   |
12 |     let b = (2.0 * x).dot(&x);
   |                  ^ no implementation for `{float} * T`
   |
   = help: the trait `std::ops::Mul<T>` is not implemented for `{float}`

I would like H * a to equal a * H for all Hilberts H. In the vein of another answer, I would try:

impl<T: Hilbert> Mul<T> for f64 {
    type Output = T;

    fn mul(self, other: T) -> T {
        other * self
    }
}

But this yields:

error[E0210]: type parameter `T` must be used as the type parameter for some local type (e.g. `MyStruct<T>`); only traits defined in the current crate can be implemented for a type parameter
  --> src/main.rs:16:1
   |
16 | / impl<T: Hilbert> Mul<T> for f64 {
17 | |     type Output = T;
18 | |
19 | |     fn mul(self, other: T) -> T {
20 | |         other * self
21 | |     }
22 | | }
   | |_^

Why is this disallowed? What's the proper way to specify commutative multiplication for a trait object?

回答1:

Why is this disallowed?

Rust enforces a policy that an implementation must be defined in the same crate as either the trait or the type. Neither Mul nor f64 are in your crate.

This prevents ambiguity about which implementation is going to be used. It makes it easy for the compiler to enforce that at most one instance of a trait exists per type, since it only has to check the implementations in those crates. If any other crate could define instances then the compiler would have to look everywhere. But also a human, trying to reason about the code, would have to be familiar with every crate, in order to guess which implementation would end up being used. Trait implementations are not named items in Rust, so you couldn't even be explicit about it. Here's some background

A common workaround is to use a wrapper type. There's zero runtime cost to doing so, but it will make the API a bit more cumbersome.

You can also define your own numeric trait, which just implies all of Add, Mul etc, implement that for all the primitive types, and use it as the bound in Hilbert instead of all the individual traits.

But this is going to be messy whichever route you go. And I would question the benefit of using the same operator for scalars, non-scalars and mixed. It would be far simpler to just add a new method to your API:

fn scale(self, by: f64) -> Self;

Apart from not getting into a complicated mess with all those trait bounds and workarounds, the intent of the code is much clearer. You won't have to look at the types of each variable to distinguish this from a multiplication of two scalars.

fn g<T: Hilbert>(x: T) -> f64 {
    let a = x.scale(2.0).dot(&x);
    let b = x.scale(2.0).dot(&x);
    a + b
}