Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I implement generic commutative std::ops involving a builtin type for trait objects?

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?

like image 571
trbabb Avatar asked Apr 23 '18 07:04

trbabb


People also ask

When should I use clone operators in generic code?

In non-generic contexts involving built-in types, this is usually not a problem. However, using these operators in generic code, requires some attention if values have to be reused as opposed to letting the operators consume them. One option is to occasionally use clone .

How should operator traits be implemented?

Implementations of operator traits should be unsurprising in their respective contexts, keeping in mind their usual meanings and operator precedence. For example, when implementing Mul, the operation should have some resemblance to multiplication (and share expected properties like associativity).

When to use the traits add<T> and add<&T>?

For example, for a user-defined type T which is supposed to support addition, it is probably a good idea to have both T and &T implement the traits Add<T> and Add<&T> so that generic code can be written without unnecessary cloning.

What are the three types of methods in C++?

These correspond to the three kinds of methods that can be invoked on an instance: call-by-reference, call-by-mutable-reference, and call-by-value. The most common use of these traits is to act as bounds to higher-level functions that take functions or closures as arguments.


1 Answers

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
}
like image 172
Peter Hall Avatar answered Nov 16 '22 03:11

Peter Hall