Skip to content

← Modules, Visibility, Testing and Docs step 3 of 22

Easy Primitives

Everything is private by default: the two privacy rules

Everything in Rust is private by default. Not “private to the file” — private to the module it is declared in, plus every module below that one. That asymmetry (children can reach up, parents cannot reach down) is the entire privacy model, and it is worth stating as the Reference states it, because two precise rules replace a lot of folklore.

An item is accessible from module m if it is public and every ancestor module of it is also accessible from m.

A private item is accessible from the module that declares it and from all of that module’s descendants.

The task

pub fn parse_ratios(pairs: Vec<(i64, i64)>) -> Vec<Option<f64>>

Inside a module named ratio, define:

  • pub struct Ratio with private fields num and den,
  • pub fn new(num: i64, den: i64) -> Option<Self>, returning None when the denominator is zero,
  • pub fn value(&self) -> f64, the quotient as a float.

parse_ratios lives at the crate root — outside ratio — and maps each pair through new and value, yielding None for any pair new rejected.

The starter compiles nothing, on purpose

The module is written for you and every item in it is private. parse_ratios sits one level up and cannot see a thing, so you get a wall of E0603 — is private. That is the error you want to be able to read fluently: it means the path is right and the visibility is wrong.

The temptation is to sprinkle pub until the noise stops. Resist it for thirty seconds and ask which three things are genuinely part of this module’s interface — and, separately, which are not. The fields are not. Adding pub to num and den also makes the code compile, and it destroys the only thing this design was for.

Why private fields matter here

With private fields, Ratio cannot be constructed from outside ratio by any route at all — not by a struct literal, not by ..Default::default(), not by accident. The only door is new, and new refuses a zero denominator. Therefore no Ratio value anywhere in the program can have a zero denominator, and value never has to check. That is what people mean by “the type makes illegal states unrepresentable”, and it is enforced by the compiler rather than by a comment.

Make num and den public and the guarantee evaporates: any caller can write Ratio { num: 1, den: 0 } and value starts returning infinity from a function that promised it wouldn’t.

A note about grading. The runtime tests here cannot see whether your fields are private — they only observe the numbers coming out, and a version with public fields produces the same numbers. What is being graded is the behaviour; what is being taught is the design. This is one of the places where you have to hold yourself to the standard, because the test cannot.

Two smaller notes for the write-up:

  • Privacy is not file-local. A private helper in mod engine is fully visible to mod engine::internals, because internals is a descendant. If you are used to Java-style package privacy this feels leaky; it is deliberate, and it is why “extract the guts into a submodule” is a safe refactor in Rust.
  • pub on a struct does not make its fields public. Structs and their fields have independent visibility, and so do enum variants’ — except that enum variants are always as public as their enum.