Item 22: Minimize visibility

Rust allows elements of the code to either be hidden from or exposed to other parts of the codebase. This Item explores the mechanisms provided for this and suggests advice for where and when they should be used.

Visibility Syntax

Rust's basic unit of visibility is the module. By default, a module's items (types, methods, constants) are private and accessible only to code in the same module and its submodules.

Code that needs to be more widely available is marked with the pub keyword, making it public to some other scope. For most Rust syntactic features, making the feature pub does not automatically expose the contents—the types and functions in a pub mod are not public, nor are the fields in a pub struct. However, there are a couple of exceptions where applying the visibility to the contents makes sense:

  • Making an enum public automatically makes the type's variants public too (together with any fields that might be present in those variants).
  • Making a trait public automatically makes the trait's methods public too.

So a collection of types in a module:

#![allow(unused)]
fn main() {
pub mod somemodule {
    // Making a `struct` public does not make its fields public.
    #[derive(Debug, Default)]
    pub struct AStruct {
        // By default fields are inaccessible.
        count: i32,
        // Fields have to be explicitly marked `pub` to be visible.
        pub name: String,
    }

    // Likewise, methods on the struct need individual `pub` markers.
    impl AStruct {
        // By default methods are inaccessible.
        fn canonical_name(&self) -> String {
            self.name.to_lowercase()
        }
        // Methods have to be explicitly marked `pub` to be visible.
        pub fn id(&self) -> String {
            format!("{}-{}", self.canonical_name(), self.count)
        }
    }

    // Making an `enum` public also makes all of its variants public.
    #[derive(Debug)]
    pub enum AnEnum {
        VariantOne,
        // Fields in variants are also made public.
        VariantTwo(u32),
        VariantThree { name: String, value: String },
    }

    // Making a `trait` public also makes all of its methods public.
    pub trait DoSomething {
        fn do_something(&self, arg: i32);
    }
}
}

allows access to pub things and the exceptions previously mentioned:

use somemodule::*;

let mut s = AStruct::default();
s.name = "Miles".to_string();
println!("s = {:?}, name='{}', id={}", s, s.name, s.id());

let e = AnEnum::VariantTwo(42);
println!("e = {e:?}");

#[derive(Default)]
pub struct DoesSomething;
impl DoSomething for DoesSomething {
    fn do_something(&self, _arg: i32) {}
}

let d = DoesSomething::default();
d.do_something(42);

but non-pub things are generally inaccessible:

let mut s = AStruct::default();
s.name = "Miles".to_string();
println!("(inaccessible) s.count={}", s.count);
println!("(inaccessible) s.canonical_name()={}", s.canonical_name());
error[E0616]: field `count` of struct `somemodule::AStruct` is private
   --> src/main.rs:230:45
    |
230 |     println!("(inaccessible) s.count={}", s.count);
    |                                             ^^^^^ private field
error[E0624]: method `canonical_name` is private
   --> src/main.rs:231:56
    |
86  |         fn canonical_name(&self) -> String {
    |         ---------------------------------- private method defined here
...
231 |     println!("(inaccessible) s.canonical_name()={}", s.canonical_name());
    |                                         private method ^^^^^^^^^^^^^^
Some errors have detailed explanations: E0616, E0624.
For more information about an error, try `rustc --explain E0616`.

The most common visibility marker is the bare pub keyword, which makes the item visible to anything that's able to see the module it's in. That last detail is important: if a somecrate::somemodule module isn't visible to other code in the first place, anything that's pub inside it is still not visible.

However, there are also some more-specific variants of pub that allow the scope of the visibility to be constrained. In descending order of usefulness, these are as follows:

  • pub(crate): Accessible anywhere within the owning crate. This is particularly useful for crate-wide internal helper functions that should not be exposed to external crate users.
  • pub(super): Accessible to the parent module of the current module and its submodules. This is occasionally useful for selectively increasing visibility in a crate that has a deep module structure. It's also the effective visibility level for modules: a plain mod mymodule is visible to its parent module or crate and the corresponding submodules.
  • pub(in <path>): Accessible to code in <path>, which has to be a description of some ancestor module of the current module. This can occasionally be useful for organizing source code, because it allows subsets of functionality to be moved into submodules that aren't necessarily visible in the public API. For example, the Rust standard library consolidates all of the iterator adapters into an internal std::iter::adapters submodule and has the following:
  • pub(self): Equivalent to pub(in self), which is equivalent to not being pub. Uses for this are very obscure, such as reducing the number of special cases needed in code-generation macros.

The Rust compiler will warn you if you have a code item that is private to the module but not used within that module (and its submodules):

#![allow(unused)]
fn main() {
pub mod anothermodule {
    // Private function that is not used within its module.
    fn inaccessible_fn(x: i32) -> i32 {
        x + 3
    }
}
}

Although the warning indicates that the code is "never used" in its owning module, in practice this warning often indicates that code can't be used from outside the module, because the visibility restrictions don't allow it:

warning: function `inaccessible_fn` is never used
  --> src/main.rs:56:8
   |
56 |     fn inaccessible_fn(x: i32) -> i32 {
   |        ^^^^^^^^^^^^^^^
   |
   = note: `#[warn(dead_code)]` on by default

Visibility Semantics

Separate from the question of how to increase visibility is the question of when to do so. The generally accepted answer to this is as little as possible, at least for any code that may possibly get used and reused in the future.

The first reason for this advice is that visibility changes can be hard to undo. Once a crate item is public, it can't be made private again without breaking any code that uses the crate, thus necessitating a major version bump (Item 21). The converse is not true: moving a private item to be public generally needs only a minor version bump and leaves crate users unaffected—read through Rust's API compatibility guidelines and notice how many are relevant only if there are pub items in play.

A more important—but more subtle—reason to prefer privacy is that it keeps your options open. The more things that are exposed, the more things there are that need to stay fixed for the future (absent an incompatible change). If you expose the internal implementation details of a data structure, a putative future change to use a more efficient algorithm becomes a breaking change. If you expose internal helper functions, it's inevitable that some external code will come to depend on the exact details of those functions.

Of course, this is a concern only for library code that potentially has multiple users and a long lifespan. But nothing is as permanent as a temporary solution, and so it's a good habit to fall into.

It's also worth observing that this advice to restrict visibility is by no means unique to this Item or to Rust:

  • The Rust API guidelines include this advice:
  • Effective Java, 3rd edition, (Addison-Wesley Professional) has the following:
    • Item 15: Minimize the accessibility of classes and members.
    • Item 16: In public classes, use accessor methods, not public fields.
  • Effective C++ by Scott Meyers (Addison-Wesley Professional) has the following in its second edition:
    • Item 18: Strive for class interfaces that are complete and minimal (my italics).
    • Item 20: Avoid data members in the public interface.