Code structure for big `impl`s distributed over several files


#1

Hi everyone!

I hope this is the right category for a little discussion :slight_smile:

My question is: Is it a good idea to do the following pattern? Is there any better way or best-practice? If not, how can I improve it? (Original publication on Hashnode).


The pattern has a few advantages:

  • hide away and bundle implementation details of one method in one file
  • debuggers won’t jump around your 3k LoC long file with you wondering where you will end up. You just have to check the filename and know at once where you are
  • arguably better overview, as one file has fewer LoC (no more CTRL+F)

And disadvantages:

  • there is boilerplate to be inserted in several file when adding one new method

So, consider a Cargo project with the following files:

// /src/main.rs

mod foo;
use foo::_struct::Foo;
use foo::*;

fn main() {
    let f = Foo{ my_var: true, };
    f.say_hello();
}
// /src/foo/mod.rs

pub mod _struct;
pub mod say_hello;
pub use self::say_hello::SayHello;
// /src/foo/_struct.rs

/**
 * Struct with all the import fields goes here!
 * I will probably have to add all the methods as comments to have one single interface overview...
 */
pub struct Foo {
    pub my_var: bool,

    /**
     * Write a friendly "Hello" to the console
     *
     * fn say_hello(&self);
     */
}
// /src/foo/say_hello.rs

use super::_struct::Foo;

pub trait SayHello {
    fn say_hello(&self);
}

impl SayHello for Foo {
    fn say_hello(&self) {
        println!("Hellow World <3");
    }
}

#2

I don’t think there’s an officially sanctioned pattern for this.
Unless there’s a need for it (like an impl defined inside a macro) I tend to keep the method and trait impls in the same file as their struct. I usually make two method impl blocks, for pub and private methods.
Try thinking in terms of modules rather than files, and see what makes more sense.


#3

@Letheed Thank you for your answer. What you write sounds interesting, but how do the advantages I listed above relate to your solution? Did you never have such a situation?

I am quite new to Rust, so I might be lacking proper awareness for composition opportunities in order to split up a module internally into decoupled units, which make logically sense in that context. That’s why I just throw a logic at Rust which I already successfully use in other, larger, productive projects written in different languages. Hence I want to know if my pattern is applicable or if developers with more experience see problems or room for improvement :slight_smile:


#4

AFAIK this is unnecessary as:

  1. You can have multiple inherent impls (impl Foo)
  2. The inherent impls don’t have to be in the same module as Foo itself.

You can just write:

// /src/main.rs

mod foo;
use foo::Foo;

fn main() {
    let f = Foo{ my_var: true, };
    f.say_hello();
}
// /src/foo/mod.rs

mod say_hello;

pub struct Foo {
    pub my_var: bool,
}
// /src/foo/say_hello.rs

use foo::Foo;

impl Foo {
    fn say_hello(&self) {
        println!("Hellow World <3");
    }
}

(Disclaimer: Untested)


#5

@troplin very interesting. I just tried it. Except for pub fn in impl it works great. I even tried to use a second impl with a different method in another file, and it scales! I am very excited to use this pattern my project.

Since, even though I did a thorough Google search on how to split one struct’s implementation into several files, I was not able to find anything about this kind of feature, I would like to write a blog post. Is it OK if I demonstrate your idea?


#6

Sure, blog posts are always a good thing.
It’s not really my idea though, the relevant RFC is here.


#7

Regarding the first point you raised: if you want to hide away some inner working of your struct, to maintain invariants for ex, you can put all the sensitive functions in a module and only expose the safe functions using pub

struct FooStruct {}

mod inner {
    use super::FooStruct;

    impl FooStruct {
        pub fn a_safe_access_function(&mut self) {
            self.a_dangerous_function();
            self.another_dangerous_function();
        }
    }

    impl FooStruct {
        fn a_dangerous_function(&mut self) {}

        fn another_dangerous_function(&mut self) {}
    }
}

impl FooStruct {
    fn another_method(&mut self) {
        self.a_safe_access_function();
        // self.a_dangerous_function(); // will not compile: function is private
        // do some more stuff
    }
}

The module system, combined with pub/private functions is here to help you establish interfaces, maintain invariants and “hide away” things. This can correlate to files but it’s not required to.
Of course you can just put inner in another file and do mod inner; instead. As @troplin said, you can pretty much put your impl blocks anywhere you want.
Regarding your second point, I kinda hate debuggers.
And for your last point, it’s a matter of personal taste. I try not to over-split files because otherwise you end up with tons of files instead of tons of lines and I don’t think it’s any better.


#8

Are you sure that doing this works? I remember trying to split an impl over multiple files, and while the compiler found a set of functions in the first impl it was unable to find the functions in the second.

Maybe this changed recently.


#9

I use this to decorate specific Results and Futures.

http://yakshav.es/decorating-results/

Example (this is a chain of futures):


#10

@Letheed

to maintain invariants for ex

my post is not really about hiding stuff from other people (in the API), but increasing overview and readability by removing code which is not connected to the logical part of the impl you are working on.

Regarding your second point, I kinda hate debuggers.

Imho, debuggers are the most critical tool when it comes to programming; how else can I watch a big program do its thing to find logical errors hidden deep inside?

I try not to over-split files

Yes! Do not oversplit them! Find a good balance :slight_smile: But even then you first need a pattern to split files in the first place.


#11

@allengeorge Yes, I tested it with rustc 1.11.0 (9b21dcd6a 2016-08-15) and it really works. I also think I tried it earlier with a different version and it did not work. If you like, I can upload the project folder!


#12

@skade I don’t get how your post is related to the thread!?


#13

No - that’s ok - I trust you :slight_smile: I think I tried it with 1.10. Interesting - I’ll give it a go again.


#14

I can form pre-conditions and post-conditions for every component or value and assert on them. Further drill down if necessary.

This works especially well when side effects are contained.

I get lost in debuggers, but I can see how people see their value. I write tests if components fail. If this isn’t possible, I refactor towards that.


#15

See the post it replies to. It uses inherent implementations for specific types to provide methods for types introduced elsewhere (in this case, libcore and futures).