0
votes

Given a struct that depends on a generic type parameter, can we define an associated function whose implementation depends on that type?

I'd like to pass a struct to a routine, but have the associated functions compute differently depending on the internal type. This routine also depends on the members in the struct, so I'd rather not move everything to a trait.

As an example, the following code attempts to define a different printme function depending on the type involved:

// Trait locked to a type
trait MyF64 {}
impl MyF64 for f64 {}
trait MyU32 {}
impl MyU32 for u32 {}

// Some kind of struct
struct Foo<T> {
    t: T,
}

// Implementation for f64
impl<T> Foo<T>
where
    T: MyF64,
{
    fn printme(&self) {
        println!("In a f64: {}", self.t);
    }
}

// Implementation for u32
impl<T> Foo<T>
where
    T: MyU32,
{
    fn printme(&self) {
        println!("In a u32: {}", self.t);
    }
}

// Takes a foo
fn foo<T>(x: Foo<T>) {
    foo.printme();
}

fn main() {
    // Try both cases
    foo(Foo { t: 1.2 });
    foo(Foo { t: 12 });
}

This gives the compiler error:

error[E0592]: duplicate definitions with name `printme`
  --> src/main.rs:17:5
   |
17 | /     fn printme(&self) {
18 | |         println!("In a f64: {}", self.t);
19 | |     }
   | |_____^ duplicate definitions for `printme`
...
27 | /     fn printme(&self) {
28 | |         println!("In a u32: {}", self.t);
29 | |     }
   | |_____- other definition for `printme`

If I move the definition of printme into another trait, we have a similar, but different problem

// Trait locked to a type
trait MyF64 {}
impl MyF64 for f64 {}
trait MyU32 {}
impl MyU32 for u32 {}

// Some kind of struct
struct Foo<T> {
    t: T,
}

// Trait for Foo
trait FooTrait {
    fn printme(&self);
}

// Implementation for f64
impl<T> FooTrait for Foo<T>
where
    T: MyF64,
{
    fn printme(&self) {
        println!("In a f64: {}", self.t);
    }
}

// Implementation for u32
impl<T> FooTrait for Foo<T>
where
    T: MyU32,
{
    fn printme(&self) {
        println!("In a u32: {}", self.t);
    }
}

// Takes a foo
fn foo<T>(x: Foo<T>)
where
    Foo<T>: FooTrait,
{
    foo.printme();
}

fn main() {
    // Try both cases
    foo(Foo { t: 1.2 });
    foo(Foo { t: 12 });
}

This gives the compiler error:

error[E0119]: conflicting implementations of trait `FooTrait` for type `Foo<_>`:
  --> src/main.rs:28:1
   |
18 | / impl<T> FooTrait for Foo<T>
19 | | where
20 | |     T: MyF64,
21 | | {
...  |
24 | |     }
25 | | }
   | |_- first implementation here
...
28 | / impl<T> FooTrait for Foo<T>
29 | | where
30 | |     T: MyU32,
31 | | {
...  |
34 | |     }
35 | | }
   | |_^ conflicting implementation for `Foo<_>`

Strictly speaking, we can fix this experiment by just adding a better trait to the types with:

// External libraries
use std::fmt::Display;

// Trait gives the name
trait MyTrait {
    fn name(&self) -> String;
}
impl MyTrait for f64 {
    fn name(&self) -> String {
        "f64".to_string()
    }
}
impl MyTrait for u32 {
    fn name(&self) -> String {
        "u32".to_string()
    }
}

// Some kind of struct
struct Foo<T> {
    t: T,
}
impl<T> Foo<T>
where
    T: MyTrait + Display,
{
    fn printme(&self) {
        println!("In a {}: {}", self.t.name(), self.t);
    }
}

// Takes a foo
fn foo<T>(x: Foo<T>)
where
    T: MyTrait + Display,
{
    x.printme();
}

fn main() {
    // Try both cases
    foo(Foo { t: 1.2 });
    foo(Foo { t: 12 });
}

which gives the correct output:

In a f64: 1.2
In a u32: 12

That said, this code is relatively simple, so this kind of fix is easy. More generally, I have a struct that depends on user defined data. This data will necessarily have a different set of associated methods and it would be difficult to force each kind of data to have a common interface. However, the struct that depends on this data can assimilate this information well as long as it knows what kind of data it has. Theoretically, we could define two different structs that accept the two different kinds of data and have these structs implement a common interface. That said, I really want access to a common set of fields and would rather not have to define a number of setters and getters. Is there a better way to accomplish this?

1
No. Define a trait with getters and setters. Or define a single common type with all the fields in common, embed that in your other types, then define a getter/setter for that one type. - Shepmaster
Note that there is nothing that prevents the user from passing a T that implements both MyF64 and MyU32, at which point the compiler can't know which printme to use. - Jmb
@Jmb That better explains what this design can't be resolved in general. Appreciated. - wyer33

1 Answers

0
votes

Well, I suppose I should have thought about this longer, but the following accomplishes what I'd like without setters and getters:

// Trait locked to a type
trait Float {
    fn bar(&self) -> String;
}
impl Float for f32 {
    fn bar(&self) -> String {
        "f32".to_string()
    }
}
impl Float for f64 {
    fn bar(&self) -> String {
        "f64".to_string()
    }
}
trait Int {
    fn baz(&self) -> &'static str;
}
impl Int for i32 {
    fn baz(&self) -> &'static str {
        "i32"
    }
}
impl Int for i64 {
    fn baz(&self) -> &'static str {
        "i64"
    }
}

// Define all of the different implementations that we care to implement in Foo
enum MyTypes {
    Float(Box <dyn Float>),
    Int(Box <dyn Int>),
}

// Some kind of struct
struct Foo {
    t : MyTypes,
}

// Implementation for f64
impl Foo {
    fn printme(&self) {
        match self.t {
            MyTypes::Float(ref t) => {
                println!("In a Float({})", t.bar());
            }
            MyTypes::Int(ref t) => {
                println!("In an Int({})", t.baz());
            }
        }
    }
}

// Takes a foo
fn foo(x : Foo) {
    x.printme();
}

fn main() {
    // Try both cases
    foo(Foo { t : MyTypes::Float(Box::new(1.2_f32))});
    foo(Foo { t : MyTypes::Int(Box::new(12_i64))});
}

Essentially, if we know that user defined data will come from a finite number of traits, we can wrap everything in an enum and then have the struct change its implementation depending on the kind of data that the user provides. This requires boxing the incoming user data based on the trait, which hides the underlying type appropriately.