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 Real
s 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 aReal
Complex
provides aninto_real()
method that returns aReal
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>
forReal
(though arguably this needs its own trait since there's more than one way to get aReal
from aComplex
) - making
project
accept animpl 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.