Public type in private mod is not documented

Hi everyone,
I just noticed that the documentation of some code I"m working on is missing some types.

I assume that this is expected behavior but I would like to now what the rules behind this behavior are. Consider the following small example:

mod foo {
    pub struct Foo {}
}

use foo::Foo;

pub fn bar() -> Foo {
    Foo {}
}

Running cargo doc --open results in a documentation containing only the function bar, however not the struct Foo.
If this was a library the user of the library could now call the method, but not actually specify the type of the return value.

  • this, if I understand the reference correctly, is due to the rule " If an item is public, then it can be accessed externally from some module m if you can access all the item's ancestor modules from m."

If Foo is made pub(crate) the compiler fails with the error 'crate-private type Foo in public interface can't leak crate-private type'.

My irritation here I think boils down to, from my understanding, very contradictory behavior of rustc and cargo doc:

  • rustc considers Foo a public type, otherwise it would fail compilation with the error from above
  • cargo doc doesn't consider Foo a public type? (Otherwise shouldn't it be documented)
  • why would I be able to call a method of which I can`t even reference the return type?

Thanks everyone :slight_smile:

Edit: after searching more, I found one use case for this 'sealed traits' Future proofing - Rust API Guidelines which uses the ability to have a public Trait which cannot be referenced and therefore not be implemented by external crates. But I don't think this use case can somehow be translated to functions/structs?

1 Like

Yeah, I'm not really a fan of how pub stuff in private modules work, including sealed traits. I'm surprised there doesn't seem to be a clippy lint for cases like this but I did find an issue for making one Lint possible leaked private structs · Issue #7301 · rust-lang/rust-clippy · GitHub

The rustc side is issue 85883, which is intentional (for things like sealed traits). Rustdoc, I'm less sure of (I didn't find an issue but there may be one).

The only use case the linked issue in the rust repo came up with for structs was to prevent external crates from calling a function via a parameter with an internal type. But you can do that with a (completely) public type that has no public constructors so I'm not really sure thats a good reason to keep things this way.

Are there other use cases for public-but-unnamable structs? I don't think I've seen them anywhere except where it appeared to be unintentional.

1 Like

I don't think I have seen then, but it appears like the good way to achieve an opposite of sealed trait: unsealed receptor.

Basically you can make functions return data structures which implement certain features, but you don't want others to accept these, instead this must be done via traits.

This way both you and others can always extend the set of types. I can imagine this to be useful for some kind of flexible GUI crate, but I'm not sure I have actually seen that pattern in the wild.

1 Like

I’ve occasionally done something similar, but using a different mechanism: In a function that returns impl Trait, it’s possible to define the returned struct, along with the relevant trait impls, inside the function body. This ensures that the type is only ever used via the given trait(s).

5 Likes

Well… that ensures that even other function from the same crate couldn't reuse these structs.

It's better if it works for you, but it's easy to imagine a slightly more complex case where you may want to return the same struct from two functions.

Then sealed struct types is the way to go. And of course you don't need or want to document them: the whole point is that they couldn't be observed except via traits they implement.

1 Like

After some tinkering I noticed that I could actually use this pattern in one of my own projects quite well:
For the project I'm programming some microcontrollers. When programming so close to the hardware there are a lot of "contracts" one has to fulfill, to just name a few:

  • reading an integrated temperature sensor asynchronously also requires the nested vector interrupt controller to be enabled and the corresponding interrupt to be enabled
  • switching on peripheral A requires peripheral B to be "properly" turned off, since they are mapped into the same region of memory
  • and the list just goes on

Many of these contracts cross the logical boundary of a single peripheral/responsibility/whatever you want to call it. The issue then is to design some "framework" which allows you to assert (at compile time) the state of specific peripherals without having to specify all peripherals in any way.

What I came up with in the end is something like the following.
The global state of the board is tracked by one (or multiple) Boards. Each Board is a map from some type Key to some runtime value Value of varying type (usually zero sized). The implementation is very similar to the one of the hlist crate.
A short example:

impl<State> Board<State> {
    pub fn init_temperature_sensor<I>(self) -> Board<State::NewTypeListType>
    where
        State: Replace<TemperatureSensorStateKey, Initialized, I, OldValueType = Uninitialized>,
    {
        let mut handle = get_temperature_sensor_handle();
        handle.interrupt_enable();
        let (new_content, _old_value) = self.state.replace(Initialized { handle });
        Board { state: new_content }
    }

    pub async fn measure_temperature<I, I2>(mut self) -> (Self, Temperature)
    where
        State: Find<TemperatureSensorStateKey, I, ValueType = Initialized>,
        State: Find<InterruptStateKey<{ Interrupt::Int012 }>, I2, ValueType = Enabled>,
    {
        let temperature: &mut Initialized =
            <State as Find<TemperatureSensorStateKey, I>>::get_mut(&mut self.state);
        let temperature = temperature.handle.start_measurement_async().await;
        return (self, temperature);
    }
}

The where bounds on the init_temperature_sensor function basically specify that the state of the board most contain a key of type TemperatureSensorStateKey whose old value is of type Uninitialized which will be replaced by an Initialiyed value.
The where bound for the measure_temperature function is similar and basically just adds the restriction that the interrupt 12 must be enabled in the nvic.

Now where do tyes which cannot be named into play?
All the state keys and values are part of the public interface so they must be public, but I don't actually want the user to create or modify any of these as they should only be used by the internals of the library to track the state of the board. Most of this could obviously also be achieved by private constructors for these types, but that would still prevent me from changing the type names.
Obviously has the downside that a user of this library cannot pass the state object anywhere, but oh well :slight_smile: