Home > Mobile >  is `as_ref` rust's ways to implement idiomatic generic?
is `as_ref` rust's ways to implement idiomatic generic?

Time:11-10

As I understand it, rust is a "has a" and not a "is a" language (composition over inheritance). this makes Liskov substitutions slightly more complicated but not impossible using Traits. While I can use LSP, it appears to not be the idiomatic way of implementing type coercion in rust. I'm left confused of how to operate.

minimal example

Let's assume I have two structs

struct Real(i32);
struct Complex(Real, Real);

And a function project which takes a Complex and return a projection of the input.

#[derive(Clone, Copy)]
struct Real(i32);
struct Complex(Real, Real);

// we pass by reference because we need to be blazingly fast
fn project(c : &Complex) -> Complex {
    Complex(c.0, Real(0))
}

fn main() {
    let a = Complex(Real(1), Real(2));
    let x = project(&a);

    println!("{}   {}i", x.0.0, x.1.0)
}

To keep things simple, please assume we are the situation in which we benefit from passing Real by reference and project should not be duplicated as multiple implementation from a trait for Real and Complex. Assume we expect to also use project on Reals from time to time.

Making project somewhat generic

My OOP instincts pushes me to make some supertype for Real and Complex, let's say the trait AsReal

#[derive(Clone, Copy)]
struct Real(i32);
struct Complex(Real, Real);

trait AsReal {
    fn as_real(&self) -> Real;
}

impl AsReal for Real { fn as_real(&self) -> Real { *self } }
impl AsReal for Complex { fn as_real(&self) -> Real { self.0 } }

fn project (r : &impl AsReal) -> Complex {
    Complex( r.as_real(), Real(0) )
}

fn main() {
    let a = Real(1);
    let b = Complex(Real(2), Real(3));

    let x = project(&a);
    let y = project(&b);
    
    println!("{}   {}i", x.0.0, x.1.0);
    println!("{}   {}i", y.0.0, y.1.0);
}

But apparently, the rusty way would be to use AsRef<Real> instead

#[derive(Clone, Copy)]
struct Real(i32);
struct Complex(Real, Real);

fn project<U: AsRef <Real>>(r : U) -> Complex {
    Complex ( *r.as_ref(), Real(0) )
}

impl AsRef<Real> for Complex {
    fn as_ref(&self) -> &Real { &self.0 }
}

impl AsRef<Real> for Real {
    fn as_ref(&self) -> &Real { self }
}

fn main() {
    let a = Real(1);
    let b = Complex(Real(2), Real(3));

    let x = project(&a);
    let y = project(&b);
    
    println!("{}   {}i", x.0.0, x.1.0);
    println!("{}   {}i", y.0.0, y.1.0);
}

Which leaves me unsatisfied : the prototype for project became very wordy and hard to read. So much so it feels like the convenience of use for project is simply not worth it.

Furthermore, it means the function must opt-in for Complex into Real coercion and I dislike that notion as it feel like it pushes me to develop defensively and use AsRef<...> all the time.

I don't feel like I have the full picture, what would be the idiomatic way to interact with rust for situation like this ?

CodePudding user response:

Based on your description, it seems like you could go with this:

  • project takes a Real
  • Complex provides an into_real() method that returns a Real

Small sidenote: if your types are small and Copy, a pointer isn't always faster. Compiler explorer can be a great tool for showing you what the assembly for a snippet is/

That being said, I'd write it like this.

fn project(real: Real) -> Real {
  // very complex logic
}

// deriving Copy makes these types behave more like numbers
#[derive(Copy, Clone)]
struct Real(i32);
#[derive(Copy, Clone)]
struct Complex(Real, Real);

impl Complex {
  fn into_real(self) -> Real {
    self.0
  }
}

fn main() {
  let real = Real(0);
  let complex = Complex(Real(0), Real(0));

  project(real);
  project(complex.into_real());
}

If you really hate having to write into_real(), you could make the call-site simpler and make the declaration-site more complex by:

  • implementing From<Complex> for Real (though arguably this needs its own trait since there's more than one way to get a Real from a Complex)
  • making project accept an impl Into<Real>
impl From<Complex> for Real {
  fn from(c: Complex) {
    c.0
  }
}

fn project(real: impl Into<Real>) {
  let real = real.into();
  // real is a `Real` here
}

Though honestly, I'd just go for the first option. Your function doesn't really need to be generic, and that increases monomorphization cost. It's not very OOP, but Rust is not an OOP language.

  • Related