Hidden type accessible in closure [type vs path privacy]

Hi, I just stumbled on this thing and I do not understand it:

pub mod mod1 {
    use self::modinner::MyType;

    mod modinner {
        pub struct MyType;

        impl MyType {
            pub fn print(&self) {
                println!("Hello World");
            }
        }
    }

    pub fn start<F: FnOnce(&MyType)>(handler: F) {
        handler(&MyType);
    }
}

pub mod mod2 {
    // use super::mod1::modinner::MyType;
    use super::mod1::start;
    pub fn test() {
        start(|x| {
            x.print();
        });
    }
}

fn main() {
    mod2::test();
}

In the closure in mod2 I am using the type MyType that in my opinion should not be accessible, because it is in a non-public module modinner. If I try to enable the commented out use super::mod1::modinner::MyType I get an error.

Why is it working in the closure?

In mod2 you can use bindings of type MyType, you just cannot explicitly provide a type for those bindings because the type is unnameable. In other words, it works because of type inference.

1 Like

Privacy only concerns what items you've allowed to name. In this case, type inference will infer that the argument type of the closure must be &MyType and let it compile without having to name it.

1 Like

This phrasing and concept is a bit ambiguous, and is probably why the OP was confused. I prefer to say that there are two things involving privacy:

  • the privacy of a type definition (itself), such as the privacy of MyType: in the OP, MyType is pub, which is why it can actually be used anywhere

  • the privacy of the paths needed to reach that type in order to name it: by having used that private modineer, the OP indeed made it impossible to directly name MyType.

The difference between the two is indeed showcased in generic / inferred contexts, where providing the names of the exact types involved is not mandatory: in that case one can still use the type itself, and any public methods or fields.

Another interesting example:

mod lib {
    pub use private::you_cant_call_me;
    mod private {
        #[derive(Debug, Default, Clone, Copy)]
        pub
        struct Hidden { x: i32 }

        pub
        fn you_cant_call_me (_: Hidden)
        {}
    }
}

fn main ()
{
    lib::you_cant_call_me(Default::default());
}
1 Like

Tangential addendum: privacy vs. fully hygienic names and paths

The following is a bit off-topic here, but it's tangential enough to warrant I mention, I'd say.

So, there is this experimental family of declarative macros, dubbed "macros 2.0" (the feature is called decl_macro), which are macros defined with the macro keyword rather than with the macro_rules! "contextual keyword".

At first sight, both of these macros behave the same, except that rules for macro macros are separated with , instead of ;, and that macro macros feature a handy single-rule shorthand syntax:

macro m( /* input */ ) {
    /* expansion */
}

But there is actually a deeper difference between macro and macro_rules! macros: hygiene. Whilst macro_rules! macros are "partially hygienic" (they use mixed_site spans) so that local variables get to be hygienic, macro macros are fully hygienic, so that even global items such as functions, types, constants, statics, modules (and other macros) are affected by hygiene as well.

This makes it so macro macros don't necessarily have to qualify, for instance, the types involved, in other to be robust.

  • This is what macro_rules! macros have to do.

    For instance, consider the following hand-rolled try_! macro, with the original semantics of ?:

    macro_rules! try_ {( $e:expr $(,)? ) => (
        match $e {
            Result::Ok(it) => it,
            Result::Err(err) => return Result::Err(err.into()),
        }
    )} use try_;
    

    and now consider the following scenario:

    type Result<T> = ::anyhow::Result<T>;
    
    fn my_func() -> Result<()> {
        let current_dir = crate::try_!( ::std::env::current_dir() );
        /* … */
        Ok(())
    }
    

    this spits:

    error[E0308]: mismatched types
      --> src/lib.rs:3:9
       |
    3  |         Result::Ok(it) => it,
       |         ^^^^^^^^^^^^^^ expected struct `std::io::Error`, found struct `anyhow::Error`
    ...
    12 |         let current_dir = try_!( ::std::env::current_dir() );
       |                           ----------------------------------
       |                           |      |
       |                           |      this expression has type `std::result::Result<PathBuf, std::io::Error>`
       |                           in this macro invocation
       |
       = note: expected enum `std::result::Result<PathBuf, std::io::Error>`
                  found enum `std::result::Result<_, anyhow::Error>`
       = note: this error originates in the macro `try_`
    

    Indeed, the try_! macro incorrectly failed to namespace the mention to Result, a global and thus unhygienic "element", which means that the name is resolved at each call site rather than at the definition site; and the call site of my example is using a different definition of Result, hence the error.

    But if we change the definition of try_! to be that of a macro macro:

    #![feature(decl_macro)]
    macro try_ {( $e:expr $(,)? ) => (
        match $e {
            Result::Ok(it) => it,
            Result::Err(err) => return Result::Err(err.into()),
        }
    )}
    

    then everything Just Works™: Playground


Thanks to that, similar to type inference, we can finally export macros which refer to publicly unnameable types:

mod private {
    pub
    enum TypeUsedByMacro {}
}

pub
macro m() {
    const _: () = {
        let _: private::TypeUsedByMacro;
    };
}

and then a downstream user can go and call m!(); without any problems whatsoever.

This is because even if the path private::TypeUsedByMacro is unnameable outside of the containing module of private thanks to privacy, the type itself is nevertheless public, and thus allowed to "escape" those module boundaries through non-path-based mechanisms. While type inference is one such mechanism, I wanted to showcase here that full hygiene is another such mechanism to escape path-based privacy.

And this is yet another example where a private type, however, won't be able to escape its containing module. Indeed, for instance, the following code:

#![feature(decl_macro)]

mod lib {
    // pub(self) /* private */
    enum TypeUsedByMacro {}

    pub
    macro m() {
        const _: () = {
            let _: TypeUsedByMacro;
        };
    }
}

lib::m!();

fails with:

error: type `TypeUsedByMacro` is private
  --> src/lib.rs:10:17
   |
10 |             let _: TypeUsedByMacro;
   |                 ^ private type
...
15 | lib::m!();
   | ---------- in this macro invocation
   |
   = note: this error originates in the macro `lib::m`
5 Likes

@Yandros this is such a superb and thorough explanation. Thanks a lot!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.