Skip to content

← Shape Your Data: Structs, Enums, Pattern Matching step 3 of 26

Easy Primitives

Tuple structs and unit structs: the other two shapes

Convert a list of measurements to metres. Each value comes with a unit string: "m" means it is already metres, "ft" means feet, and anything else is unrecognised and normalises to 0.0.

pub fn normalise_distances(values: Vec<f64>, units: Vec<String>) -> Vec<f64>

One foot is exactly 0.3048 metres. If the two vectors have different lengths, stop at the shorter one. Compare with a tolerance — this is floating point.

Three shapes of struct, not one

You have met the named-field struct. There are two more, and beginners skip both and then need them constantly.

struct Meters(f64);   // tuple struct  — fields are .0, .1, ...
struct Unknown;       // unit struct   — no fields at all

A tuple struct is a struct whose fields are numbered instead of named. With exactly one field it is called a newtype, and it is one of the most used patterns in Rust. Meters(f64) and Feet(f64) both wrap the same machine representation, but they are different types. Hand a Feet to a function expecting Meters and it does not compile. That is the entire point: the unit lives in the type, so a unit mix-up becomes a build failure rather than a Mars orbiter.

A unit struct has no data at all. size_of::<Unknown>() is 0 — it occupies no memory and costs nothing to pass around. Its whole content is its identity: it is a name you can attach an impl to. Later in the course you will use them as typestate markers; here one serves as a stand-in for “we do not recognise this unit”.

Converting between them: From

Implement From<Feet> for Meters and you get a real conversion:

impl From<Feet> for Meters {
    fn from(feet: Feet) -> Self {
        Meters(feet.0 * 0.3048)
    }
}

feet.0 is how you read a tuple struct’s first field. Writing feet.value is a compile error, E0609 — the starter ships exactly that so you meet it once, deliberately.

Implementing From<A> for B gives you B::from(a) and a.into() for free, because the standard library has a blanket Into implementation that builds on From. Prefer writing From; you get both directions of the call site out of it.

The trick worth stealing

A tuple struct’s name also lives in the value namespace — it is an ordinary function of its fields. So this works, and is what an experienced Rust programmer writes:

let metres: Vec<Meters> = raw.into_iter().map(Meters).collect();

Not .map(|x| Meters(x)). Meters is the function fn(f64) -> Meters. The same is true of tuple-shaped enum variants: .map(Some), .map(Ok), .map(Event::Write).

And in the other direction, you can pull the value straight back out with a pattern, in a plain let:

let Meters(m) = converted;

Naming

upper_case_acronyms will reject struct KM(f64). Rust’s convention is Km, Http, Uuid — acronyms are written like ordinary words in type names.

Remember the grade is compile + tests + clippy -D warnings.