-1

I'm struggling in dynamic dispatch for the whole day. try to work trait object with generic parameter, I know that can't be made into object. But I still want to write Object-Oriented code. For writing code, there are many Box<dyn ?> types in code source and is ugly and tedious. When I change &dyn Exchanger:

    fn reduce(&self, exchanger: &dyn Exchanger, dest: &Unit) -> Result<Amount, Error>;

To use generic parameter style, rust is compliant in dyn Expression implementation.

    fn reduce<E: Exchanger>(&self, exchanger: &E, dest: &Unit) -> Result<Amount, Error> 
        where Self: Sized;

I also try to write static dispatch version in day, but I found I dig into generic recursion issue: Sum<Sum<Sum<...>,...>. is there a better way to write code in rust. How can I fix the problem? thanks. The whole code as below:

use std::fmt::{Debug, Display};

trait Boxed {
    fn boxed(self) -> Box<Self>
    where
        Self: Sized,
    {
        Box::new(self)
    }
}

impl<T> Boxed for T {}

type DynExpression = Box<dyn Expression>;

#[derive(Debug)]
enum Error {}

trait Exchanger {
    fn rate(&self, source: &Unit, dest: &Unit) -> Result<u32, Error>;
}

trait Expression: Debug + Display {
    fn add(self: Box<Self>, addend: DynExpression) -> DynExpression;

    fn times(self: Box<Self>, multiplier: u32) -> DynExpression;

    fn reduce<E: Exchanger>(&self, exchanger: &E, dest: &Unit) -> Result<Amount, Error> 
        where Self: Sized;
}

impl Expression for DynExpression {
    fn add(self: Box<Self>, addend: DynExpression) -> DynExpression {
        (*self).add(addend)
    }

    fn times(self: Box<Self>, multiplier: u32) -> DynExpression {
        (*self).times(multiplier)
    }

    fn reduce<E: Exchanger>(&self, exchanger: &E, dest: &Unit) -> Result<Amount, Error>
    where
        Self: Sized,
    {
        let trait_object: &dyn Expression = &**self;
        // compiler error
        Expression::reduce(trait_object, exchanger, dest)
    }
}

#[derive(Debug, Clone, PartialEq)]
struct Amount {
    amount: u32,
    unit: Unit,
}

impl Amount {
    fn new(amount: u32, unit: Unit) -> Self {
        Amount { amount, unit }
    }
}

impl Display for Amount {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{amount}{unit}", amount = self.amount, unit = self.unit)
    }
}

impl Expression for Amount {
    fn add(self: Box<Self>, addend: DynExpression) -> DynExpression {
        Sum(self as DynExpression, addend).boxed()
    }

    fn times(self: Box<Self>, multiplier: u32) -> DynExpression {
        Amount::new(self.amount * multiplier, self.unit).boxed()
    }

    fn reduce<E: Exchanger>(&self, exchanger: &E, dest: &Unit) -> Result<Amount, Error> {
        if self.unit == *dest {
            return Ok(self.clone());
        }
        let rate = exchanger.rate(&self.unit, dest)?;
        Ok(Amount::new(self.amount * rate, dest.clone()))
    }
}

#[derive(Debug, Clone, PartialEq)]
struct Unit {
    key: String,
}

impl Unit {
    fn new<K: Into<String>>(key: K) -> Unit {
        Unit { key: key.into() }
    }
}

impl Display for Unit {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&self.key)
    }
}

#[derive(Debug, Clone, PartialEq)]
struct Sum<L, R>(L, R);

impl<L: Display, R: Display> Display for Sum<L, R> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{lhs} + {rhs}", lhs = self.0, rhs = self.1)
    }
}

impl<L, R> Expression for Sum<L, R>
where
    L: Expression + 'static,
    R: Expression + 'static,
{
    fn add(self: Box<Self>, addend: DynExpression) -> DynExpression {
        Sum(self as DynExpression, addend).boxed()
    }

    fn times(self: Box<Self>, multiplier: u32) -> DynExpression {
        Sum(
            self.0.boxed().times(multiplier),
            self.1.boxed().times(multiplier),
        )
        .boxed()
    }

    fn reduce<E: Exchanger>(&self, exchanger: &E, dest: &Unit) -> Result<Amount, Error> {
        let (lhs, rhs) = (
            self.0.reduce(exchanger, dest)?,
            self.1.reduce(exchanger, dest)?,
        );
        Ok(Amount::new(lhs.amount + rhs.amount, dest.clone()))
    }
}

#[cfg(test)]
mod tests {

    use super::*;

    fn kg() -> Unit {
        Unit::new("kg")
    }

    fn g() -> Unit {
        Unit::new("g")
    }

    #[test]
    fn unit_to_string() {
        assert_eq!(g().to_string(), "g");
        assert_eq!(kg().to_string(), "kg");
    }

    #[test]
    fn amount_to_string() {
        assert_eq!(Amount::new(1, g()).to_string(), "1g");
        assert_eq!(Amount::new(5, kg()).to_string(), "5kg");
    }

    #[test]
    fn sum_to_string() {
        let one = Amount::new(1, g()).boxed();
        let five = Amount::new(5, kg()).boxed();
        let sum = one.add(five);

        assert_eq!(sum.to_string(), "1g + 5kg");
    }

    #[test]
    fn add_amount_with_same_unit() {
        let one = Amount::new(1, g()).boxed();
        let five = Amount::new(5, g()).boxed();

        let result = one.clone().add(five.clone());
        assert_eq!(result.to_string(), "1g + 5g");
    }

    #[test]
    fn amount_multiplication() {
        let five = Amount::new(5, g()).boxed();

        let result = five.times(3);

        assert_eq!(result.to_string(), "15g");
    }

    #[test]
    fn sum_add_amount() {
        let one = Amount::new(1, g()).boxed();
        let two = Amount::new(2, g()).boxed();
        let five = Amount::new(5, kg()).boxed();

        let result = one.clone().add(five.clone());
        let result = result.add(two.clone());

        assert_eq!(result.to_string(), "1g + 5kg + 2g");
    }

    #[test]
    fn sum_multiplication() {
        let one = Amount::new(1, g()).boxed();
        let five = Amount::new(5, kg()).boxed();

        let result = one.clone().add(five.clone()).times(3);

        assert_eq!(result.to_string(), "3g + 15kg");
    }

    #[test]
    fn reduce_amount_to_same_unit() {
        let one = Amount::new(1, g()).boxed();

        let result = one.reduce(&Weight, &g()).unwrap();
        assert_eq!(result, *one);
    }

    #[test]
    fn reduce_amount_to_diff_unit() {
        let one = Amount::new(1, kg());

        let result = one.reduce(&Weight, &g()).unwrap();
        assert_eq!(result, Amount::new(1000, g()));
    }

    #[test]
    fn reduce_sum_to_same_unit() {
        let one = Amount::new(1, g()).boxed();
        let five = Amount::new(5, g()).boxed();

        let sum = one.add(five);

        let result = sum.reduce(&Weight, &g()).unwrap();
        assert_eq!(result, Amount::new(6, g()));
    }

    #[test]
    fn reduce_sum_to_diff_unit() {
        let one = Amount::new(1, kg()).boxed();
        let five = Amount::new(5, g()).boxed();

        let sum = one.add(five);

        let result = sum.reduce(&Weight, &g()).unwrap();
        assert_eq!(result, Amount::new(1005, g()));
    }

    struct Weight;

    impl Exchanger for Weight {
        fn rate(&self, source: &Unit, dest: &Unit) -> Result<u32, Error> {
            match (&*source.key, &*dest.key) {
                ("kg", "g") => Ok(1000),
                _ => todo!(),
            }
        }
    }
}

Static dispatch version in rust

Static dispatch approach needs break Expression into Expression, Product and Reduce to solve the compiler error type annotation needed.

use std::fmt::{Debug, Display};
trait Boxed {
    type Output;

    fn boxed(self) -> Self::Output;
}

impl<T> Boxed for T {
    type Output = Self;

    fn boxed(self) -> Self::Output {
        self
    }
}

trait Exchanger {
    type Err;

    fn rate(&self, source: &Unit, dest: &Unit) -> Result<u32, Self::Err>;
}

trait Product {
    type Output;

    fn times(self, multiplier: u32) -> Self::Output;
}

trait Reduce {
    type Output;

    fn reduce<E: Exchanger>(&self, exchanger: &E, dest: &Unit) -> Result<Self::Output, E::Err>;
}

trait Expression<Rhs = Self> {
    fn add(self, addend: Rhs) -> Sum<Self, Rhs>
    where
        Self: Sized;
}

#[derive(Debug, Clone, PartialEq)]
struct Amount {
    amount: u32,
    unit: Unit,
}

impl Amount {
    fn new(amount: u32, unit: Unit) -> Self {
        Amount { amount, unit }
    }
}

impl Display for Amount {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{amount}{unit}", amount = self.amount, unit = self.unit)
    }
}

impl<Rhs> Expression<Rhs> for Amount {
    fn add(self, addend: Rhs) -> Sum<Self, Rhs>
    where
        Self: Sized,
    {
        Sum(self, addend)
    }
}

impl Product for Amount {
    type Output = Self;
    fn times(self, multiplier: u32) -> Self::Output {
        Amount::new(self.amount * multiplier, self.unit)
    }
}

impl Reduce for Amount {
    type Output = Amount;

    fn reduce<E: Exchanger>(&self, exchanger: &E, dest: &Unit) -> Result<Self::Output, E::Err> {
        if self.unit == *dest {
            return Ok(self.clone());
        }
        Ok(Amount::new(
            self.amount * exchanger.rate(&self.unit, dest)?,
            dest.clone(),
        ))
    }
}

#[derive(Debug, Clone, PartialEq)]
struct Unit {
    key: String,
}

impl Unit {
    fn new<K: Into<String>>(key: K) -> Unit {
        Unit { key: key.into() }
    }
}

impl Display for Unit {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(&self.key)
    }
}

#[derive(Debug, Clone, PartialEq)]
struct Sum<L, R>(L, R);

impl<L: Display, R: Display> Display for Sum<L, R> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{lhs} + {rhs}", lhs = self.0, rhs = self.1)
    }
}

impl<L, R, Rhs> Expression<Rhs> for Sum<L, R> {
    fn add(self, addend: Rhs) -> Sum<Self, Rhs>
    where
        Self: Sized,
    {
        Sum(self, addend)
    }
}

impl<L, R> Reduce for Sum<L, R>
where
    L: Reduce<Output = Amount>,
    R: Reduce<Output = Amount>,
{
    type Output = Amount;

    fn reduce<E: Exchanger>(&self, exchanger: &E, dest: &Unit) -> Result<Self::Output, E::Err> {
        let (lhs, rhs) = (
            self.0.reduce(exchanger, dest)?,
            self.1.reduce(exchanger, dest)?,
        );

        Ok(Amount::new(lhs.amount + rhs.amount, lhs.unit))
    }
}

impl<L, R> Product for Sum<L, R>
where
    L: Product<Output = L>,
    R: Product<Output = R>,
{
    type Output = Self;
    fn times(self, multiplier: u32) -> Self::Output
    where
        Self: Sized,
    {
        Sum(self.0.times(multiplier), self.1.times(multiplier))
    }
}

#[cfg(test)]
mod tests {

    use super::*;

    fn kg() -> Unit {
        Unit::new("kg")
    }

    fn g() -> Unit {
        Unit::new("g")
    }

    #[test]
    fn unit_to_string() {
        assert_eq!(g().to_string(), "g");
        assert_eq!(kg().to_string(), "kg");
    }

    #[test]
    fn amount_to_string() {
        assert_eq!(Amount::new(1, g()).to_string(), "1g");
        assert_eq!(Amount::new(5, kg()).to_string(), "5kg");
    }

    #[test]
    fn sum_to_string() {
        let one = Amount::new(1, g()).boxed();
        let five = Amount::new(5, kg()).boxed();
        let sum = one.add(five);

        assert_eq!(sum.to_string(), "1g + 5kg");
    }

    #[test]
    fn add_amount_with_same_unit() {
        let one = Amount::new(1, g()).boxed();
        let five = Amount::new(5, g()).boxed();

        let result = one.clone().add(five.clone());
        assert_eq!(result.to_string(), "1g + 5g");
    }

    #[test]
    fn amount_multiplication() {
        let five = Amount::new(5, g()).boxed();

        let result = five.times(3);

        assert_eq!(result.to_string(), "15g");
    }

    #[test]
    fn sum_add_amount() {
        let one = Amount::new(1, g()).boxed();
        let two = Amount::new(2, g()).boxed();
        let five = Amount::new(5, kg()).boxed();

        let result = one.clone().add(five.clone());
        let result = result.add(two.clone());

        assert_eq!(result.to_string(), "1g + 5kg + 2g");
    }

    #[test]
    fn add_sum2() {
        let one = Amount::new(1, g()).boxed();
        let two = Amount::new(2, g()).boxed();
        let five = Amount::new(5, kg()).boxed();

        let sum1 = one.clone().add(five.clone());
        let sum2 = one.clone().add(two.clone());
        // compiler error: need type annotation
        let result = sum1.add(sum2);

        assert_eq!(result.to_string(), "1g + 5kg + 1g + 2g");
    }

    #[test]
    fn sum_multiplication() {
        let one = Amount::new(1, g()).boxed();
        let five = Amount::new(5, kg()).boxed();

        let result = one.clone().add(five.clone());
        let result = result.times(3);

        assert_eq!(result.to_string(), "3g + 15kg");
    }

    #[test]
    fn reduce_amount_to_same_unit() {
        let one = Amount::new(1, g()).boxed();

        let result = one.reduce(&Weight, &g()).unwrap();
        assert_eq!(result, one);
    }

    #[test]
    fn reduce_amount_to_diff_unit() {
        let one = Amount::new(1, kg());

        let result = one.reduce(&Weight, &g()).unwrap();
        assert_eq!(result, Amount::new(1000, g()));
    }

    #[test]
    fn reduce_sum_to_same_unit() {
        let one = Amount::new(1, g()).boxed();
        let five = Amount::new(5, g()).boxed();

        let sum = one.add(five);

        let result = sum.reduce(&Weight, &g()).unwrap();
        assert_eq!(result, Amount::new(6, g()));
    }

    #[test]
    fn reduce_sum_to_diff_unit() {
        let one = Amount::new(1, kg()).boxed();
        let five = Amount::new(5, g()).boxed();

        let sum = one.add(five);

        let result = sum.reduce(&Weight, &g()).unwrap();
        assert_eq!(result, Amount::new(1005, g()));
    }

    struct Weight;

    impl Exchanger for Weight {
        type Err = ();

        fn rate(&self, source: &Unit, dest: &Unit) -> Result<u32, ()> {
            match (&*source.key, &*dest.key) {
                ("kg", "g") => Ok(1000),
                _ => todo!(),
            }
        }
    }
}

In the end, I switch to use std::ops::Add and std::ops::Mul instead, the final code in github repo. Is there any better way to do in static/dynamic dispatch approach?

tadman
  • 208,517
  • 23
  • 234
  • 262
holi-java
  • 29,655
  • 7
  • 72
  • 83
  • 3
    Don't try to force OOP patterns into Rust. – Chayim Friedman Aug 23 '23 at 11:47
  • 1
    The code in SO question should be **minimal**. For example, the trait `Boxed` is not related to your issue, right? It is just a convenience wrapper over `Box::new()`. We can argue whether this is a good idea or not, but when asking on SO, you should strip such things. – Chayim Friedman Aug 23 '23 at 11:50
  • BTW, `impl Expression for Box` is usually a bad idea. Better to impl it generically for `Box where E: Expression + ?Sized`, and you get this implementation for free. – Chayim Friedman Aug 23 '23 at 11:55
  • 2
    Using static polymorphism is usually the best approach. You should avoid dynamic polymorphism in most cases. Please show us your attempt with generics. – Chayim Friedman Aug 23 '23 at 11:56
  • 1
    I stand with @ChayimFriedman: your biggest problem is "[oop does not fit Rust] but [you] still want to write Object-Oriented code". It's not going to end well. – jthulhu Aug 23 '23 at 12:56
  • @ChayimFriedman I following your suggestion, but the [code](https://github.com/holi-java/amount/blob/main/src/lib.rs#L31) can't run anymore. I need your help, thanks. – holi-java Aug 23 '23 at 13:03
  • @jthulhu thanks, I found [tag:golang] and [tag:java] write [tag:oo] code more easily. I want to solve the problem of SKU with multi units. is there a way to go there? e.g: for quantity of the stock in database saved as `1kg tomato => 1000g tomato` with base unit: `g`, but for display to users `1200g tomato => 1kg + 200g tomato`. – holi-java Aug 23 '23 at 13:05
  • The code you linked still uses dynamic dispatch. Also, please post the code for the static dispatch _in the question_, not as a link to GitHub or any other external resource. – Chayim Friedman Aug 23 '23 at 13:11
  • 2
    @holi-java if you are used to writing OO code, it can be hard to get used to how Rust works. My advice would be: 1) Stop trying to think in OO terms. It's hard, but *at least* you should not try to go in the OO direction. 2) Read the [Rust book](https://doc.rust-lang.org/book/). It's very well written, to the point, and covers everything you need to know to get started. 3) Start with an easier problem, with a single `main` function where you do everything in it. Progressively, use tools shown in the book to make your code better. This way, you avoid OO patterns. – jthulhu Aug 23 '23 at 13:13
  • @jthulhu Maybe I haven't turn my brains to stop thinking [tag:oo], :) . Is there some advanced books talk about that? – holi-java Aug 23 '23 at 13:22
  • @holi-java I don't know of any book that specifically talks about transitioning from OOP to Rust-like languages, but I know of a research article that shows different possible "extension" directions (unfortunately I have to refind it...). It's helpful as it shows design perspectives that are usually not mentioned in OOP. You can google those terms, or read [this answer](https://stackoverflow.com/questions/76411040/a/76424401) (if you can read OCaml), which talks about this topic too. – jthulhu Aug 23 '23 at 13:31
  • @ChayimFriedman sir, I back to use static dispatch, but the [code](https://github.com/holi-java/amount/blob/static-dispatch/src/lib.rs) can't run anymore. please help me, thanks. – holi-java Aug 23 '23 at 13:37
  • Sure it can't run, you put `todo!()` all over the place. Also, it seems you haven't read what I said: put the code _in the question_. – Chayim Friedman Aug 23 '23 at 13:39
  • @ChayimFriedman sir, `todo!()` can't make the compiler failure, since the never type `!` can assign to any type in rust, errors occurs in `Sum::reduce` and `Sum::add` type system that needs type annotation... – holi-java Aug 23 '23 at 13:51
  • You said it doesn't run, not that it doesn't compile. And I didn't include the tests in my check. And it's just a "type annotation needed" error, not something unsolveable. There is probably a better way to place the generic, though, than on `Expression`. – Chayim Friedman Aug 23 '23 at 14:05
  • @ChayimFriedman Sorry for my bad english. I'm solved the type system problem by break `Expression` into 3 `trait`s, is there another better way to do that, sir. – holi-java Aug 23 '23 at 14:38
  • It's worth noting that Rust likes to use *composition* instead of *inheritance* and once you start structuring things that way it gets way easier. Composition often means "adding traits" or "using an `enum` for variants". – tadman Aug 23 '23 at 14:47
  • @tadman Thanks, sir. the code of the static dispatch version is more clearly. Can you tell me any advanced resource talking about rust type system? I come from China, can't google any thing... – holi-java Aug 23 '23 at 14:58
  • There are some good books to introduce Rust concepts, and there's the [online book](https://doc.rust-lang.org/book/) as well. – tadman Aug 23 '23 at 15:11
  • 1
    @tadman Thanks, maybe I need to read the official book more than once, ^_^. – holi-java Aug 23 '23 at 15:33

0 Answers0