Dynamic Generic Type

I wasn't sure what title to give to this question but my issue goes on like this.. In the number enum below I want to add a fraction variant to it however I don't want to add generics to Number but I want it do be like this Fraction(GenericFraction<dyn Clone + Integer>). My goal is that GenericFraction where T is any type that has the required trait bounds , can be made into the Number enum

use standardform::StandardForm;
use fraction::{GenericFraction,Integer};

#[derive(Debug,Clone,PartialEq,PartialOrd)]
pub enum Number {
    Decimal(f64),

    StandardForm(StandardForm),

    Fraction(GenericFraction<dyn Clone + Integer>)
}

This is my Cargo.toml


[dependencies]
standardform = "0.1.1"
fraction = { version = "0.13.1" , default-features = false }

However I get this error

error[E0225]: only auto traits can be used as additional traits in a trait object
  --> src\number.rs:11:42
   |
11 |     Fraction(GenericFraction<dyn Clone + Integer>)
   |                                  -----   ^^^^^^^ additional non-auto trait
   |                                  |
   |                                  first non-auto trait
   |
   = help: consider creating a new trait with all of these as supertraits and using that trait here instead: `trait NewTrait: Clone + Integer {}`
   = note: auto-traits like `Send` and `Sync` are traits that have special properties; for more information on them, visit <https://doc.rust-lang.org/reference/special-types-and-traits.html#auto-traits>

error[E0038]: the trait `Integer` cannot be made into an object
  --> src\number.rs:11:30
   |
11 |     Fraction(GenericFraction<dyn Clone + Integer>)
   |                              ^^^^^^^^^^^^^^^^^^^ `Integer` cannot be made into an object
   |

The first error is quite easy to solve , by using the following and only implemting this trait for primitives like u8...i64

trait Primitive : Clone + Integer {

}

But the second one is quite tough.
So it there a work around or an alternative way to acheive what I am trying to do.

Much like the old "Doctor, it hurts when I do this" joke, the solution is: don't do that. As in, don't use non-object safe traits with dyn.

You will need to design your own Primitive trait to be object safe and have the operations you need.

(FYI: Clone is also not object safe.)

1 Like

Another complication: Trait objects in Rust are designed to abstract over a single value of a given type. Whereas for a Rational type, you would usually want to have two values of the same type, presumably. It might even be necessary to go into unsafe code and manually handing vtables to make something like this work.

Edit: actually on second thought… that’s not such a huge problem, as one can just combine the denominator and numerator first and then abstract over the whole rational as a single object. However then, it’s also not easy to define things like adding/multiplying/… two such dynamic rationals, unless you have any story of handling the case where their underlying number types are different.

So basically the recommendation / solution is just don't use it , rather use something like Fraction(GenericFraction<u32>). But out of curiousity, if I 'only' wanted that dyn .. thing , how would I do that, as in other langauges this is not much of an issue

Let’s talk about this conceptually: If your traits would define how to add u32s to u32s and how to add BigInts to BigInts, if now both u32 and BigInt can become dyn SomeTraitForNumbers and you have a DynFraction(Fraction<dyn SomeTraitForNumbers>) kind of type, what happens if you add one value containing u32s (for the numerator and denominator) with another value containing BigInts?

How would your example in unnamed “other languages” handle this case? And how would you want it handled? The answer to this question might inform what approach could work in Rust, too.

3 Likes

And how would you want it handled?

For me , I think just dyn type T + dyn type O = type Output is the behaviour I would like to do this.

For example in kotlin it could be approached like the following
Note : This is code will not compile , and assumes the required code is availiable . It just outlines the basic possible implementation of my 'approch'


// f64 is Double class in kotlin
class StandardForm(/*...*/)
class Fraction<T>(/*...*/) {
    val typeOfGeneric : KClazz<T> = /**/
}

// Then maybe like 
sealed class Number {
    class Fraction<T>(numerator : T , denomiator : T/*...*/) {
     val typeOfGeneric : KClazz<T> = /**/
   }
   class StandardForm(/*...*/) {
 		val value = StandardForm(/*..*/)
   },
   object Decimal {
 		val value = 12.0
   }
}

// assuming this interface is implemented for the requires types 
// needed as kotlin doesn't have something like fn<T : std::ops::Add> add() -> Self ...
interface Add<T,Self> {
    fun add(other : T) -> Self
}

// Helper fuction
// needed as kotlin doesn't have something like fn<T : std::ops::Add> add() -> Self ...
fun <T : Any> hasInterface(kClass: KClass<T>, targetInterface: KClass<*>): Boolean {
    return targetInterface.java.isAssignableFrom(kClass.java)
}

// Then 
fun Number.add(other : Number) -> Number {
	return when {
            this is Number.Decimal && other is Number.Decimal -> this.value + other.value
            this is Fraction && other is Fraction -> if hasInterface(this.typeOfGeneric,Add::class) && hasInterface(other.typeOfGeneric,Add::class){
                val n = this.numerator + other.numerator;
                val d = other.denominator + this.denominator
                
                // THis can be simpilifed by using a custom constructor but I just havent done that
                Number.Fraction(n,d)
            }
            // in rust you would return Err(...)
            else throw Error("CANT DO IT")
            this is StandardForm && other is StandardForm -> this + other
            // For simplicity ignornig the other combinations
            else -> TODO("...")
        }
}

If I understand you right (as I couldn't fully understand the details of the Kotlin code), dyn type T + dyn type O = type Output is achievable via double dynamic dispatch. You'd have to decide how you want to handle the "leaf cases", i.e., how you'd pair specific types.

You may want to convert everything to a common type, practically the same as the result type, in which case this is as simple as:

trait DynAdd {
    type Output;

    fn as_output(&self) -> Self::Output;

    fn dyn_add(&self, other: &dyn DynAdd<Output = Self::Output>) -> Self::Output
    where
        Self::Output: Add<Self::Output, Output = Self::Output>
    {
        self.as_output() + other.as_output()
    }
}
1 Like

If I understand you right (as I couldn't fully understand the details of the Kotlin code)

You understood it correctly that a + b = c

And the example helped me understand the whole thing in rust

FYI : In the number enum I've decided to use Fraction(GenericFraction<u32>) as it seems the most apporpiate for a general use case

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.