Private trait in public interface: sometimes allowed and sometimes not?

Hmmmm, interesting. So I was tempted to say that if the lint was turned into a hard error in future, then I couldn't use generic impls for code deduplication unless I give the user of my module a way to write down a bound that covers the implementations that use the deduplication. But that's not entirely true. Consider the following code:

mod api {
    #[derive(Default)]
    pub struct A {
        internal: i32,
    }
    #[derive(Default)]
    pub struct B {
        internal: i32,
    }
    pub struct Dbl<T> {
        pub one: T,
        pub two: T,
    }
    // This trait is public:
    pub trait SomePublicTrait {
        fn internal_ref(&self) -> &i32;
        fn internal_mut(&mut self) -> &mut i32;
    }
    impl SomePublicTrait for A {
        fn internal_ref(&self) -> &i32 {
            &self.internal
        }
        fn internal_mut(&mut self) -> &mut i32 {
            &mut self.internal
        }
    }
    impl SomePublicTrait for B {
        fn internal_ref(&self) -> &i32 {
            &self.internal
        }
        fn internal_mut(&mut self) -> &mut i32 {
            &mut self.internal
        }
    }
    pub trait Trt {
        fn increment(&mut self);
        fn result(&self) -> i32;
    }
    impl<T: SomePublicTrait> Trt for T {
        fn increment(&mut self) {
            *self.internal_mut() += 1;
        }
        fn result(&self) -> i32 {
            *self.internal_ref()
        }
    }
    impl<T: SomePublicTrait> Dbl<T> {
        // And we use `SomePublicTrait` for code deduplication here,
        // but function `sum` is private anyway:
        fn sum(&self) -> i32 {
            self.one.result() + self.two.result()
        }
    }
    pub struct Foo {
        pub dbl_a: Dbl<A>,
        pub dbl_b: Dbl<B>,
    }
    impl Foo {
        pub fn product(&self) -> i32 {
            self.dbl_a.sum() * self.dbl_b.sum()
        }
    }
}

fn main() {
    use api::*;
    let mut dbl_a = Dbl {
        one: A::default(),
        two: A::default(),
    };
    dbl_a.one.increment();
    dbl_a.two.increment();
    dbl_a.two.increment();
    let mut dbl_b = Dbl {
        one: B::default(),
        two: B::default(),
    };
    dbl_b.one.increment();
    dbl_b.one.increment();
    let foo = Foo { dbl_a, dbl_b };
    println!("{}", foo.product());
}

(Playground)

The deduplicated function sum isn't public. So there's no need outside the api module to express bounds on types providing that function.

Yet the lint apparently forces us to make SomePublicTrait public. But do we really need to make it public in that case? Apparently not, as we can do:

         pub one: T,
         pub two: T,
     }
-    // This trait is public:
-    pub trait SomePublicTrait {
+    trait Internal {
         fn internal_ref(&self) -> &i32;
         fn internal_mut(&mut self) -> &mut i32;
     }
-    impl SomePublicTrait for A {
+    impl Internal for A {
         fn internal_ref(&self) -> &i32 {
             &self.internal
         }
@@ -24,7 +23,7 @@
             &mut self.internal
         }
     }
-    impl SomePublicTrait for B {
+    impl Internal for B {
         fn internal_ref(&self) -> &i32 {
             &self.internal
         }
@@ -36,7 +35,7 @@
         fn increment(&mut self);
         fn result(&self) -> i32;
     }
-    impl<T: SomePublicTrait> Trt for T {
+    impl<T: Internal> Trt for T {
         fn increment(&mut self) {
             *self.internal_mut() += 1;
         }
@@ -44,10 +43,12 @@
             *self.internal_ref()
         }
     }
-    impl<T: SomePublicTrait> Dbl<T> {
-        // And we use `SomePublicTrait` for code deduplication here,
-        // but function `sum` is private anyway:
-        fn sum(&self) -> i32 {
+    // Note that this trait is private:
+    trait SumViaTrait {
+        fn sum_via_trait(&self) -> i32;
+    }
+    impl<T: Internal> SumViaTrait for Dbl<T> {
+        fn sum_via_trait(&self) -> i32 {
             self.one.result() + self.two.result()
         }
     }
@@ -57,7 +58,7 @@
     }
     impl Foo {
         pub fn product(&self) -> i32 {
-            self.dbl_a.sum() * self.dbl_b.sum()
+            self.dbl_a.sum_via_trait() * self.dbl_b.sum_via_trait()
         }
     }
 }

(Playground)

Here sub_via_trait is private (as well as sum in the previous example), and we don't have SomePublicTrait in our published API anymore. We also don't use any tricks with public items in private modules.

So I think there's a solution for all cases. It just involves introducing and using traits (either public or private, depending on the application case). The latter feels a bit clunky in Rust sometimes, but the "pub use Trait in _" syntax may come in handy when dealing with a lot of traits. Hence why I now have in my real-life code:

/// Unnameable traits on data types for wildcard import
pub mod traits {
    pub use super::{Env as _, EnvBuilderOpen as _, Txn as _};
}

Btw, I hate that syntax. Maybe it would be nice if some types could automatically bring certain (sealed) traits into scope. But that would require to have sealed traits being part of the language first.

1 Like