Tedious Trait Implementations

Hello!

So I have a trait that I want to implement for multiple structs.
The base behavior is really simple but implementing all of it manually
would create lots of unnecessary code and would be incredibly tedious.

Here is a small example:

pub trait Block {

    fn hello() -> i8;

    fn hellos() -> Vec<i8>;

    fn hi() -> i32;

}

pub struct SimpleBlock;

impl Block for SimpleBlock {
    // tedious
}

pub struct CoolBlock;

impl Block for CoolBlock {
    // tedious
}

pub struct InterestingBlock;

impl Block for InterestingBlock {
    // tedious
}

pub struct NiceBlock;

impl Block for NiceBlock {
    // tedious
}


fn main() {

}

Is there a way around implementing it manually every single time?
I know that it's possible with macros but due to the limitation that you need a dedicated crate
for macros, it's quite hard to get them into my existing project.

Best regards,
Dominik

It's not clear from your example code what exactly you're trying to achieve but I think what you want is to provide a default implementation of those functions in your trait:

pub trait Block {
  fn hello() -> i8 {
    0
  }

  fn hellos() -> Vec<i8> {
    vec![]
  }

  fn hi() -> i32 {
    0
  }
}

which you can then implement and override as necessary:

pub struct SimpleBlock;

impl Block for SimpleBlock { } // uses trait definition defaults

pub struct CoolBlock;

impl Block for CoolBlock { // overrides hello but uses the other defaults
  fn hello() -> i8 {
    42
  }
}

I know that it's possible with macros but due to the limitation that you need a dedicated crate
for macros, it's quite hard to get them into my existing project.

You're probably thinking of procedural macros but for this kind of use case, macro_rules! are much simpler and can be defined inside your existing crate.

3 Likes

I know about default implementations.
The problem is that the code in my actual application is way more complex than just returning an integer.
It also needs a state but an interface can not have fields.

Your impl blocks are “tedious” to write, but not sufficiently similar to use the simple anti-repetition mechanisms like default implementations. It will be hard to suggest an alternative without understanding the similarities and differences between your different objects; can you elaborate on what you’re actually trying to do?

5 Likes

If one trait is a superset of another trait, you can use blanket implementation. I had a case when I had 2 traits and I needed an umbrella trait that encompassed both traits. Blanket implementation made that easy.

See Storing elements in a Vec where each element implements multiple dyn Traits - #10 by qaopm and the discussion.

I wrote a long rant here but I think I'll save it for an eventual blog post. The gist is: this question feels like an inheritance-shaped hole in a design that probably isn't ideal for Rust. To fix this we need to know two things: what is the program supposed to do, and how does Block fit into a design to achieve that goal?

Thanks for all of your replies.
I'm quite new to rust and sorry if I'm asking silly questions but I think this community is awesome!

A block should represent a section having several sub-blocks.
There should be different blocks having different functionality.
That's why I made a trait called Block and in the real world application
it has parent & children methods.
The different functionalities should be represented by different structs implementing the block trait.

Aren't you looking for a custom derive?

1 Like

custom derive is very hard to implement in my case because I'd need another crate

I don't think your questions are silly, on the contrary, they're very important to answer for newcomers to Rust! What's difficult is that they're very open ended and so there are lots of different answers that have various tradeoffs.

Just some really high-level feedback: you've pretty accurately described the design you want but you haven't really described the problem you're trying to solve. What are these blocks? Are they blocks of text? Blocks in a video game? How do they relate to what your program does?

The reason we're asking is because the design you're asking for feels very much like Java style OO which doesn't tend to translate very well to Rust for a variety of reasons. Rather than trying to figure out how to express that design in Rust which is going to cause more problems for you later (wanting parent/child references in some kind of a tree can be quite difficult or at least tedious to make work in Rust), we should help you figure out a more idiomatic design in Rust which solves the actual problem.

5 Likes

I'm coming from the Java world :slight_smile: , the blocks should be blocks of text.

1 Like

Okay, but what do they do? What problem does your program solve and how does Block fit in?

Design starts with requirements. The design of a typesetting engine is going to be different than the design of a natural language processing library, even though both deal with "blocks of text". They have different architectures that are a consequence of different requirements. They solve different problems.

What's the problem your program solves? How does Block relate to other parts of the program's structure?

1 Like

A block just stores the next within it and parses other blocks within it.
Here is a short example:

Persons { // A persons block
  Person { // A person block
    My nice text.
    Very cool.
  }
  Other nice text
}

I'm not an expert by any means but I've found that the tidiest way to do this kind of thing in rust is for the CoolBlock struct to contain a Block, then

let mut my_cool_block = CoolBlock();
... // later
my_cool_block.block.hi()

If CoolBlock.hi() differs from Block.hi() then the function can call the component struct function before or after doing its additional processing. I've found the process of sorting my functionality to enable this to work nicely (c.f. the approach of Java, Python, C#) is very useful.

Nope. That's only true if you're planning to use what are called "procedural" macros:

''derive'' example
    #[derive(Serialize)] // like this one
    struct Data {
        num: i32
    }

They generate a bunch of code, starting from what you feed into it.

And they do require separate, heavy libraries, along with separate crates - to hold them.

There is another tool in Rust's macro arsenal however, which is much more suitable for your case:

macro_rules!

The example from the page is as simple as it gets:

expansion to a ''println!''
    // This is a simple macro named `say_hello`.
    macro_rules! say_hello {
        // `()` indicates that the macro takes no argument.
        () => {
            // The macro will expand into the contents of this block.
            println!("Hello!");
        };
    }

    fn main() {
        // This call will expand into `println!("Hello");`
        say_hello!()
    }

In your case, I'd write something like this:

macro for your trait
    pub trait Block {
        fn hello() -> i8;
        fn hellos() -> Vec<i8>;
        fn hi() -> i32;
    }
    macro_rules! implement {
        ( 
            $(  
                $struct_name:tt {
                    => $fn1:expr 
                    => $fn2:expr
                    => $fn3:expr
                }
            ),* 
        ) => {
            $(
                // #[allow(dead_code)] 
                // uncomment to disable warnings
                pub struct $struct_name;

                impl Block for $struct_name {
                    fn hello() -> i8 {
                        $fn1
                    }
                    fn hellos() -> Vec<i8> {
                        $fn2
                    }
                    fn hi() -> i32 {
                        $fn3
                    }
                }
            )*
        }
    }

This would be used as follows:

expansion into your example
    implement!{
        SimpleBlock {
            => 8
            => Vec::new()
            => 32
        },
        CoolBlock {
            => 4
            => Vec::new()
            => 64
        },
        InterestingBlock {
            => 6
            => Vec::new()
            => 128
        }
        // and so on
    }
7 Likes

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.