0

1.) I have made a big knot in my code it seems. I defined my own structs e.g.

struct State { // some float values }

and require them be multiplied by f64 + Complex64 and added together member-wise. Now I tried to abstract away the f64 and Complex64 into a trait called "Weight" and I have two structs that need to implement them:

struct WeightReal {
   strength: f64,
}
struct WeightComplex {
   strength: num_complex::Complex<f64>,
}

Now it's more complicated as I need custom multiplication for these Weights with my struct "State" AND also with f64 itself (because I do other things as well). So I need "Weight" x State and "Weight" x f64 for both possible weight-types. Do I have to define all of these multiplications myself now? I used the derive_more-crate in the past, but I think now it's at its limits. Or I fundamentally misunderstood something here. Another question is: Do I need to define a struct here? I tried type-aliases before but I think there was an error, because I couldn't define custom multiplication on type aliases (at least it seemed so to me). It could've just been me doing it incorrectly.

  1. The Rust way of defining multiplication / overloading of the "*"-operator somehow flies right over my head. With "cargo expand" I looked at a multiplication derived through the derive_more-crate:
impl<__RhsT: ::core::marker::Copy> ::core::ops::Mul<__RhsT> for State
where
    f64: ::core::ops::Mul<__RhsT, Output = f64>,
{
    type Output = State;
    #[inline]
    fn mul(self, rhs: __RhsT) -> State {
        State {
            value1: <f64 as ::core::ops::Mul<__RhsT>>::mul(self.value1, rhs),
            value2: <f64 as ::core::ops::Mul<__RhsT>>::mul(self.value2, rhs),
        }
    }
}

If someone could explain a few of the parts here: what does the "<f64 as ::core.... "-part mean? I understand that "__RhsT" means "Right-Hand-Side-Type", but I don't understand why it is still generic, because in this example shouldn't it be specifically f64? The third line is also puzzling me, why is it necessary?

I am really confused. The rust docs regarding multiplication are also unclear to me as they seem to be abstracted away in some macro.

Peter Hall
  • 53,120
  • 14
  • 139
  • 204
exocortex
  • 375
  • 2
  • 9

1 Answers1

3

There is a lot of noise in the generated code, which is fairly typical of code from macros. That's in order to reduce the chance of naming conflicts or remove ambiguity if there are several traits in scope with the same method name.

This is a bit more readable:

use std::ops::Mul;

impl<Rhs: Copy> Mul<Rhs> for State
where
    f64: Mul<Rhs, Output = f64>,
{
    type Output = State;

    fn mul(self, rhs: Rhs) -> State {
        State {
            value1: <f64 as Mul<Rhs>>::mul(self.value1, rhs),
            value2: <f64 as Mul<Rhs>>::mul(self.value2, rhs),
        }
    }
}

f64::mul(a, b) is another way to call a method, a.mul(b), while being precise about exactly which mul function you mean. That's needed because it's possible for there to be multiple possible methods with the same name. These could be inherent, from different traits, or from different parametrisations of the same trait.

Rhs is a geneneric parameter rather than just f64 because it's possible to implement Mul serveral times for the same type, using different type parameters. For example, it is reasonably to multiply an f64 by another f64, but it also makes sense to multiply by an f32, u8, i32 etc. Implementing Mul<u8> for f64 allows you to do 1.0f64 * 1u8.

<f64 as Mul<Rhs>>::mul(a, b) is specifying to call the mul method of Mul where the left hand side is an f64, but where the right hand side, Rhs, can be any type.


As for your first question, it's hard to understand what you are actually attempting, but the difficulty may hint that implementing Mul is not the right thing to do in the first place. If you have several different ways to multiply, then perhaps you should just have a different method for each one. It will probably end up being clearer and simpler. There isn't a big advantage in being able to use the * operator.

Peter Hall
  • 53,120
  • 14
  • 139
  • 204
  • Thank you! I think I am now nearer to understanding this. I am still unsure about the "as" keyword, as I thought it is about casting. Regarding my actual project: I am implementing a network solver for delay differential equations. Where dynamical systems that are influencing each other through a network are simulated. I want to be able to add different types of systems later that can still use that common interface. State of one system is then multiplied by a "weight" and feeds into another system. Different systems can require different types of weights. – exocortex Oct 28 '22 at 15:12