Organising modules inside crates


#1

I’m writing my first serious crate and I’m having some trouble deciding how to organise the various traits, types and functions into modules. I was hoping for some guidance about idiomatic ways to handle this in Rust.
As an example, suppose my crate has types Producer and Consumer; Consumer implements a trait called Interface, which is used by Producer to send its output. My questions is: how should I organise these types into modules? My first instinct is to avoid cycles in the module usage graph, and since both Producer and Consumer depend on Interface, yet should be in separate modules, it means the Interface should either be in the same module as Producer or Consumer, or in a different module altogether. So far I’ve chosen the latter, but it’s quickly getting out of hand (there are many interfaces…).

So… any thoughts? :smile:


#2

Don’t worry about cycles in the module dependency graph. The language can handle it. And as long as you aren’t making every single thing pub in every module, then they are still serving their purpose.

What is their purpose?

Believe it or not: Encapsulation. That is to say, modules are Rust’s privacy barriers. People coming from object-oriented languages might be used to thinking of the datatype as the unit of encapsulation, but this is not the case in Rust. After all, any function inside the same module can see the type’s private fields, while methods implemented on the type outside of its module cannot see anything.

Therefore, you should let your needs for encapsulation define your module boundaries, and then (this is my assertion) your code organization should come out mostly nice as well. I say, don’t be afraid to use inner modules (mod { ... }) if you need a really quick barrier around something, and you can use pub(crate) use self::module::thing; to bubble names up the module tree so that other code doesn’t need to be affected too much by ad-hoc namespacing.

(Here is an actual 15-line inner utility module I have sitting somewhere in the middle of my own code, just to make it easy to verify that, indeed, it is impossible to borrow the contents)

use self::black_hole::BlackHole;
mod black_hole {
    use std::fmt;

    /// Contains something that is dangerous to obtain references to.
    ///
    /// It will never be seen again (except to be dropped).
    pub struct BlackHole<T>(T);
    impl<T> BlackHole<T> {
        pub fn entrap(x: T) -> BlackHole<T> { BlackHole(x) }
    }

    impl<T> fmt::Debug for BlackHole<T> {
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
            write!(f, "BlackHole(_)")
        }
    }
}

To address your particular problem, I’m not sure I understand: Are there multiple consumers and producers, or is there really just one of each? (or do you at least plan to have multiple consumers?)

If there’s just one, then I have to wonder why the interfaces exist (as it would seem likely to me that they are tightly coupled to the Consumer, and thus provide little benefit compared to having the Producer just use the Consumer directly; at least, as I am imagining it).

If there’s more than one, I guess I would probably keep the traits close to the things that implement them. Like

// (break into files/directories as needed)

mod producer {
    use consumer::traits::{Foo, Bar};
    struct Producer;

    impl Producer {
        // methods
    }
}

mod consumers {

    // the consumer traits
    pub trait Foo { }
    pub trait Bar { }

    // the consumers
    pub use self::a::ConsumerA;
    mod a {
        struct ConsumerA {
            data: Stuff,
        }
        // impl the traits here
    }

    pub use self::b::ConsumerB;
    mod b {
        struct ConsumerB {
            data: Stuff,
        }
        // impl the traits here
    }
}

Then again, if each trait is only used by, say, a single function of Producer, then I guess I might prefer to put the traits there (as they can be considered part of the signature of those functions). So I really can’t tell you! Traits are not strongly affected by privacy concerns, so it tends not to matter where you put them. :slight_smile:


#3

Wow, thanks for the detailed response! The privacy barrier is something I didn’t think of before. I’ll have to think a bit more about it.

As for my particular situation, I’m writing an assembler. There are always at least two implementations of a trait: the useful one and the test double. :smile:
I also use traits as a form of encapsulation. For example, a Token contains location information (useful for diagnostics), but the Parser shouldn’t care about that at all. I have something like this:

trait TerminalSymbol {
    fn kind(&self) -> TerminalKind;
}

enum TerminalKind {
    Comma,
    Identifier,
    // ...
}

struct Token {
    // ...
}

impl TerminalSymbol for Token {
    // ...
}

This way I know exactly what properties of tokens are relevant for parsing, and that makes it a lot easier to test the parser.