Trait with private items

Hi,

I would like to make a trait some items private and some public. I have considered to use sealed super trait, but in my case the private items depend on public ones, so it didn't work (also, I think it would not make private items inaccessible).

I would like something like this:

// The trait should be visible by user and usable in bounds.
// It cannot be implemented by user, but that's fine.
pub trait MyTrait {
    // This type should be public
    type Item;
    
    // This type should be private, it is an implementation detail,
    // completely invisible to user, it should not be a breaking
    // change to change or remove this.
    type Iter: Iterator<Item = Self::Item>;
    
    // This function should be private, it is an implementation detail,
    // completely invisible to user, it should not be a breaking
    // change to change or remove this.
    fn get_item(&mut self) -> Self::Item;
}

// There are functions like this in my library
pub fn do_something<T: MyTrait>(value: T) -> T::Item {
    todo!()
}

// ... and some implementations of MyTrait
struct Foo;
impl MyTrait for Foo { /* ... */ }

I am currently considering two approaches:

  • Just #[doc(hidden)] the private items (and maybe add extra unnameable-typed argument to functions).
  • Keep them public and document them (maybe adding a "but pretend you do not see this" note).

I don't like any of them. Is there anything better?

That's sealing for methods, basically. std::error::Error does it.

For types you could seal a supertrait and make e.g. Item a parameter of the trait. Note that (whatever the approach) a private associated type clashes with being dyn compatible. It might work better to put bounds on the sealed methods instead of using a supertrait.

What exactly does "private" mean here?

Can you make your own extension trait with those extra items, and that extension trait could be private?

Nice, that would solve the dependency issue, thanks.

... but now that I play with it, it seems that the private associated types are easily accessible:

pub mod lib {
    mod inner {
        pub trait MyTraitInner<Item> {
            type Iter: Iterator<Item = Item>;
            fn get_item(&mut self) -> Item;
        }
    }

    pub trait MyTrait: inner::MyTraitInner<Self::Item> {
        type Item;
    }
}

// This compiles fine, not even a warning,
// but T::Iter should be private
fn test<T: lib::MyTrait>(iter: T::Iter) {
    todo!()
}

So how is that any better than putting #[doc(hidden)] there? Or am I doing it wrong?

That's fine in my case. The trait is already dyn incompatible anyway (not shown in OP example).

No, the private parts are not easily (if at all) expressible as an extension trait.

In my library I have a function and a set of types that can be used as an argument of that function. But how exactly do those types interact with that function is an internal implementation detail, which I do not want to expose to users.

Making separate function for each type seems infeasible in my case (but I may rethink it).

IIUC you want a sealed trait:

pub trait MyTrait: sealed::Sealed {
    type Iter: Iterator<Item = Self::Item>;
}

mod sealed {
    pub trait Sealed {
        type Item;
        fn get_item(&mut self) -> Self::Item;
    }
}

// works
pub fn do_something<T: MyTrait>(value: T) -> T::Item {
    todo!()
}

Try a bound on the method instead of the supertrait bound.

        #[doc(hidden)]
        fn get_iter(
            &mut self,
            _: private::SealedArg,
        ) -> <Self as private::DefIter<Self::Item>>::Iter
        where
            Self: private::DefIter<Self::Item>;

// ...
// optionally
    trait ForLocalConvenience: MyTrait + private::DefIter<Self::Item> {}
    impl<T> ForLocalConvenience for T
    where
        T: MyTrait + private::DefIter<T::Item> + ?Sized
    {}

Make your private trait private:

pub mod lib {
    trait MyTraitInner<Item> {
        type Iter: Iterator<Item = Item>;
        fn get_item(&mut self) -> Item;
    }
    
    #[allow(private_bounds)]
    pub trait MyTrait: MyTraitInner<Self::Item> {
        type Item;
    }
}

// Doesn't compile.
fn test1<T: lib::MyTrait>(iter: T::Iter) {
    todo!()
}

// Doesn't compile.
fn test2<T: lib::MyTrait>(mut a: T) {
    a.get_item();
}

Wow, this looks too good to be true, where's the catch? Why do we even consider sealed traits if this works?

I didn't know about this one. Is it something new? Can it be relied upon? I have troubles finding docs for it, can you share a link?

Using a private supertrait started being allowed in Rust 1.74 with this "type privacy" RFC. private_bounds is now just a warning, not an error. If you don't allow it, the code will still compile with a warning. I believe the "wrap a public trait in a private module" hack is no longer necessary.

You used to do exactly the same thing by putting a pub trait in a non-pub module. It's been a classic hack for ages; it's just now easier to do by not needing the bonus module.

But it's not the same. If you do that, the two functions test1 and test2 will compile, demonstrating the trait isn't really private:

pub mod lib {
    mod hidden {
        pub trait MyTraitInner<Item> {
            type Iter: Iterator<Item = Item>;
            fn get_item(&mut self) -> Item;
        }
    }
    
    pub trait MyTrait: hidden::MyTraitInner<Self::Item> {
        type Item;
    }
}

// This compiles.
fn test1<T: lib::MyTrait>(iter: T::Iter) {
    todo!()
}

// This also compiles.
fn test2<T: lib::MyTrait>(mut a: T) {
    a.get_item();
}

So I implemented this in my project and it works well, thank you. Also thanks to @quinedot for suggesting a parameter on supertrait.

I think this issue would benefit from a clearer motivation; if the ::Iter associated type really needs to be that private, then why does it need to appear in the public trait? I really do not think that an actually-private super-trait is the way to go, here. In other words, the private_bounds warning does hint at there being something smelly.

I suspect that what you want is mere convenience, locally, as in specifying in one go both your public trait, and the private version thereof.

If so, this post from quinedot gets there in one way:

I'd add to this / adjust it a bit further, though:

  • given your current setup, your private trait need not be generic over an Item, but, instead, be a subtrait of the public trait.

  • you could then either blanket-implement it, if/when applicable, or have manual impls thereof.

    • if blanket-implemented, you should be able to, locally, keep using the public trait as a bound and yet have access to the private subtrait associated type (you'll just need to use fully qualified syntax rather than the shorthand): given <T : PubSuperTrait>, Rust will know that T : SubTrait also holds thanks to the blanket impl.
    • if manually/case-by-case implemented, then, in order to get that assumedly desired local convenience, you will need to be using the private subtrait as the bound in your generic APIs (<T : SubTrait>).
  • Keep in mind that even if it is a subtrait, you will still be capable of defining logic of the super/public trait in terms of the subtrait for local implementors or whatnot.

Example

pub
trait Pub {
    type Item;
    fn get_item(&mut self) -> Option<Self::Item>;
}

trait Private : Pub {
    type Iter : Iterator<Item = Self::Item>;
    
    fn get_iter(&mut self) -> &mut Self::Iter;
}

pub
struct Example<I : Iterator>(
    I,
);

impl<I : Iterator> Private for Example<I> {
    type Iter = I;
    
    fn get_iter(&mut self) -> &mut I {
        &mut self.0
    }
}

impl<I : Iterator> Pub for Example<I> {
    type Item = I::Item;
    
    fn get_item(&mut self) -> Option<I::Item> {
        self.get_iter().next()
    }
}

The case where I could see the private subtrait as legitimate is when, on top of the aforementioned things:

  • you wanted to expose some dedicated <T : PubTrait> API, such as a generic fn, which, in the implementation, would get to assume that T : PrivateTrait holds, whilst there being no blanket implementation of PrivateTrait, and instead, a manual/case-by-case list of these.

  • you'd be fine with, or even find it desirable, for downstream users not to be able to add their own impls of PubTrait.

In such a case, the PubTrait : PrivateTrait hierarchy makes sense, and if so, an actually (type-)private super trait shall be more honest, and thus, work better in edge cases and diagnostics, than a path-private/unnameable SealedTraitHackā„¢ trait.

But that conclusion can only be reached with these extra requirements and assumptions, which should be spelled out in this thread, if anything, for others stumbling into it :slightly_smiling_face:

It's always a pleasure to see you pop up from time to time. Selfishly, I wish it happened more frequently, lol.