Step 5 of 6
Tuple structs, newtypes and unit structs
Sometimes naming every field is overkill. A tuple struct has a name but its fields are only numbered, like a tuple:
struct Rgb(u8, u8, u8);
fn main() {
let orange = Rgb(255, 140, 0);
println!("red {}, green {}, blue {}", orange.0, orange.1, orange.2);
}
red 255, green 140, blue 0
Note the definition ends with ; (there are no braces), and you create one like a function call: Rgb(255, 140, 0).
Newtypes: the same number, different meanings
The most useful tuple struct has just one field. Wrapping a plain type to give it a meaning is called the newtype pattern:
struct Meters(f64);
struct Feet(f64);
fn to_meters(length: Feet) -> Meters {
Meters(length.0 * 0.3048)
}
fn climb(height: Meters) {
println!("climbing {:.1} m", height.0);
}
fn main() {
let wall = Feet(100.0);
climb(to_meters(wall));
climb(Meters(8848.0));
}
climbing 30.5 m
climbing 8848.0 m
Meters and Feet both hold an f64, but to the compiler they are different types. You can't pass feet where meters are expected:
struct Meters(f64);
struct Feet(f64);
fn climb(height: Meters) -> f64 {
height.0
}
fn main() {
let wall = Feet(30.0);
println!("{}", climb(wall));
}
error[E0308]: mismatched types
This is not a small thing. In 1999 NASA lost the Mars Climate Orbiter because one program produced numbers in pound-force seconds and another read them as newton-seconds. Both were just floating point numbers, so nothing caught it. With newtypes, that mix-up is a compile error. The same idea at work: UserId(u64) and OrderId(u64) so you can never pass an order id to delete_user, or Cents(u32) so nobody adds dollars to cents. A newtype costs nothing at run time: in memory it's exactly the value inside.
The price: a newtype doesn't do arithmetic or printing until you teach it (Meters(3.0) * 2.0 is error[E0369]: cannot multiply `Meters` by `{float}` ). For now, reach inside with .0; the standard library traits module shows how to add operators.
Unit structs
A struct can even have no fields at all: struct Ping;. This unit struct holds no data and takes no memory. It's useful as a marker, or as a type to hang behavior on with traits, later in the course. You create one by writing its name: let p = Ping;.
Your task
Write fn to_fahrenheit(c: Celsius) -> Fahrenheit using the formula F = C × 9 / 5 + 32.
Challenge 2
Time units, as newtypes. Write:
fn to_seconds(m: Minutes) -> Seconds(1 minute is 60 seconds)fn add(a: Seconds, b: Seconds) -> Seconds
Challenge 3
Money is usually stored as a whole number of cents (floating point can't represent 0.10 exactly). Write a newtype and two functions from scratch:
- a tuple struct
Centsholding au32, with#[derive(Debug, PartialEq)] fn add(a: Cents, b: Cents) -> Centsfn display(c: &Cents) -> String, which formats the amount as dollars with exactly two decimals:Cents(1205)gives"$12.05",Cents(7)gives"$0.07".
Tip: convert to f64 only for display, c.0 as f64 / 100.0, and format with {:.2}.
Previous: Printing structs with Debug Next: Putting it together: an inventory