058: Variants — Days of the Week
Difficulty: 1 Level: Beginner Model a finite set of named values as an enum with exhaustive pattern matching.The Problem This Solves
Days of the week are a fixed, finite set. Using `&str` or `i32` to represent them means you can pass `"Funday"` or `8` and the compiler won't stop you. A typo compiles. An invalid value compiles. An enum makes invalid values unrepresentable. `Day::Funday` doesn't compile. `Day::from_index(8)` returns `None`. The compiler checks every `match` is exhaustive — if you add `Day::Holiday` later, every match arm that forgot to handle it becomes a compile error, not a runtime surprise. This is the algebraic data type applied to the simplest case: a sum type with no data attached to each variant (called a fieldless or C-like enum).The Intuition
Variants are named constants that the type system tracks. Instead of `1 = Monday, 2 = Tuesday`, you have `Day::Mon, Day::Tue`. The compiler knows exactly which values exist. The `match` expression forces you to handle all of them. In OCaml, you get equality, printing, and copying for free. In Rust, you opt into traits with `#[derive]`. Same concept, more explicit declaration.How It Works in Rust
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Day { Sun, Mon, Tue, Wed, Thu, Fri, Sat }
// ^ derive gives: print, copy, compare — all free
impl Day {
pub fn name(self) -> &'static str {
match self {
Day::Sun => "Sunday", Day::Mon => "Monday",
// ... compiler errors if any variant is missing
Day::Sat => "Saturday",
}
}
pub fn is_weekend(self) -> bool {
matches!(self, Day::Sun | Day::Sat) // matches! macro for boolean patterns
}
pub fn next(self) -> Day {
match self { Day::Sat => Day::Sun, Day::Sun => Day::Mon, /* ... */ }
}
// Arithmetic alternative (avoids exhaustive match):
pub fn next_arithmetic(self) -> Day {
Day::from_index((self as u8 + 1) % 7).unwrap()
}
}
impl std::fmt::Display for Day {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name()) // enables format!("{day}")
}
}
`Copy` makes `Day` cheaply copyable (no heap allocation). The `as u8` cast gives access to the discriminant for arithmetic.
What This Unlocks
- Type-safe domain values — directions, states, card suits, HTTP methods — any fixed set of named things.
- Exhaustive handling — the compiler guarantees you've handled every variant; add a new one and find every match that needs updating.
- State machines — enums with methods like `next()` are the foundation for finite state automata.
Key Differences
| Concept | OCaml | Rust | ||
|---|---|---|---|---|
| Declaration | `type day = Sun \ | Mon \ | ...` | `enum Day { Sun, Mon, ... }` |
| Debug printing | Automatic | `#[derive(Debug)]` required | ||
| Equality | Automatic | `#[derive(PartialEq, Eq)]` required | ||
| Copy semantics | Automatic | `#[derive(Clone, Copy)]` required | ||
| Methods | Free functions | `impl Day { fn name(self) ... }` | ||
| Display | Manual or ppx | `impl std::fmt::Display for Day` |
//! # Variants — Days of the Week
//!
//! OCaml variants map cleanly to Rust enums. Both are algebraic data types
//! with exhaustive pattern matching enforced by the compiler.
// ---------------------------------------------------------------------------
// Approach A: Idiomatic Rust — enum with methods via impl block
// ---------------------------------------------------------------------------
/// Days of the week as a simple C-like enum.
///
/// We derive common traits that OCaml variants get implicitly:
/// - `Debug` for printing
/// - `Clone, Copy` because this is a simple fieldless enum
/// - `PartialEq, Eq` for equality comparison
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Day {
Sun,
Mon,
Tue,
Wed,
Thu,
Fri,
Sat,
}
impl Day {
/// Human-readable name.
pub fn name(self) -> &'static str {
match self {
Day::Sun => "Sunday",
Day::Mon => "Monday",
Day::Tue => "Tuesday",
Day::Wed => "Wednesday",
Day::Thu => "Thursday",
Day::Fri => "Friday",
Day::Sat => "Saturday",
}
}
/// Is this a weekend day?
pub fn is_weekend(self) -> bool {
matches!(self, Day::Sun | Day::Sat)
}
/// Next day of the week (wraps around).
pub fn next(self) -> Day {
match self {
Day::Sun => Day::Mon,
Day::Mon => Day::Tue,
Day::Tue => Day::Wed,
Day::Wed => Day::Thu,
Day::Thu => Day::Fri,
Day::Fri => Day::Sat,
Day::Sat => Day::Sun,
}
}
}
// ---------------------------------------------------------------------------
// Approach B: Functional style — free functions with pattern matching
// ---------------------------------------------------------------------------
/// Mirrors the OCaml `day_name` function — a standalone function,
/// not a method.
pub fn day_name(d: Day) -> &'static str {
// Identical logic but as a free function rather than a method.
// In OCaml all functions on types are free functions.
d.name()
}
pub fn is_weekend(d: Day) -> bool {
d.is_weekend()
}
pub fn next_day(d: Day) -> Day {
d.next()
}
// ---------------------------------------------------------------------------
// Approach C: Numeric representation — using discriminants
// ---------------------------------------------------------------------------
impl Day {
/// Convert from a 0-based index (Sun=0 .. Sat=6).
pub fn from_index(i: u8) -> Option<Day> {
match i {
0 => Some(Day::Sun),
1 => Some(Day::Mon),
2 => Some(Day::Tue),
3 => Some(Day::Wed),
4 => Some(Day::Thu),
5 => Some(Day::Fri),
6 => Some(Day::Sat),
_ => None,
}
}
/// Convert to a 0-based index.
pub fn to_index(self) -> u8 {
self as u8
}
/// Next day using arithmetic modulo — avoids exhaustive match.
pub fn next_arithmetic(self) -> Day {
Day::from_index((self.to_index() + 1) % 7).unwrap()
}
}
/// Display trait for pretty-printing.
impl std::fmt::Display for Day {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_day_names() {
assert_eq!(Day::Sun.name(), "Sunday");
assert_eq!(Day::Wed.name(), "Wednesday");
assert_eq!(Day::Sat.name(), "Saturday");
}
#[test]
fn test_is_weekend() {
assert!(Day::Sun.is_weekend());
assert!(Day::Sat.is_weekend());
assert!(!Day::Mon.is_weekend());
assert!(!Day::Wed.is_weekend());
assert!(!Day::Fri.is_weekend());
}
#[test]
fn test_next_day() {
assert_eq!(Day::Sun.next(), Day::Mon);
assert_eq!(Day::Wed.next(), Day::Thu);
assert_eq!(Day::Sat.next(), Day::Sun); // wraps around
}
#[test]
fn test_next_full_cycle() {
// Going through all 7 days should return to start
let mut d = Day::Mon;
for _ in 0..7 {
d = d.next();
}
assert_eq!(d, Day::Mon);
}
#[test]
fn test_arithmetic_next() {
assert_eq!(Day::Sun.next_arithmetic(), Day::Mon);
assert_eq!(Day::Sat.next_arithmetic(), Day::Sun);
// Should agree with pattern-match version for all days
for i in 0..7 {
let d = Day::from_index(i).unwrap();
assert_eq!(d.next(), d.next_arithmetic());
}
}
#[test]
fn test_from_index_invalid() {
assert_eq!(Day::from_index(7), None);
assert_eq!(Day::from_index(255), None);
}
#[test]
fn test_display() {
assert_eq!(format!("{}", Day::Fri), "Friday");
}
}
fn main() {
println!("{:?}", Day::Sun.name(), "Sunday");
println!("{:?}", Day::Wed.name(), "Wednesday");
println!("{:?}", Day::Sat.name(), "Saturday");
}
(* Variants — Days of the Week *)
type day = Sun | Mon | Tue | Wed | Thu | Fri | Sat
(* Implementation 1: Pattern matching functions *)
let day_name = function
| Sun -> "Sunday" | Mon -> "Monday" | Tue -> "Tuesday"
| Wed -> "Wednesday" | Thu -> "Thursday" | Fri -> "Friday"
| Sat -> "Saturday"
let is_weekend = function
| Sun | Sat -> true
| _ -> false
let next_day = function
| Sun -> Mon | Mon -> Tue | Tue -> Wed | Wed -> Thu
| Thu -> Fri | Fri -> Sat | Sat -> Sun
(* Implementation 2: Using integer encoding *)
let day_to_int = function
| Sun -> 0 | Mon -> 1 | Tue -> 2 | Wed -> 3
| Thu -> 4 | Fri -> 5 | Sat -> 6
let int_to_day = function
| 0 -> Sun | 1 -> Mon | 2 -> Tue | 3 -> Wed
| 4 -> Thu | 5 -> Fri | 6 -> Sat
| _ -> failwith "invalid day index"
let next_day_arith d = int_to_day ((day_to_int d + 1) mod 7)
(* Tests *)
let () =
assert (day_name Wed = "Wednesday");
assert (is_weekend Sat = true);
assert (is_weekend Mon = false);
assert (next_day Sat = Sun);
assert (next_day Wed = Thu);
assert (next_day_arith Sat = Sun);
(* Full cycle *)
let d = ref Mon in
for _ = 1 to 7 do d := next_day !d done;
assert (!d = Mon);
Printf.printf "All variants-days tests passed!\n"
📊 Detailed Comparison
Comparison: Variants — Days of the Week — OCaml vs Rust
OCaml
🐪 Show OCaml equivalent
type day = Sun | Mon | Tue | Wed | Thu | Fri | Sat
let day_name = function
| Sun -> "Sunday" | Mon -> "Monday" | Tue -> "Tuesday"
| Wed -> "Wednesday" | Thu -> "Thursday" | Fri -> "Friday"
| Sat -> "Saturday"
let is_weekend = function
| Sun | Sat -> true
| _ -> false
let next_day = function
| Sun -> Mon | Mon -> Tue | Tue -> Wed | Wed -> Thu
| Thu -> Fri | Fri -> Sat | Sat -> Sun
Rust — Idiomatic (impl methods)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Day { Sun, Mon, Tue, Wed, Thu, Fri, Sat }
impl Day {
pub fn name(self) -> &'static str {
match self {
Day::Sun => "Sunday", Day::Mon => "Monday", /* ... */
}
}
pub fn is_weekend(self) -> bool {
matches!(self, Day::Sun | Day::Sat)
}
pub fn next(self) -> Day {
match self {
Day::Sun => Day::Mon, /* ... */ Day::Sat => Day::Sun,
}
}
}Rust — Numeric (discriminant arithmetic)
impl Day {
pub fn from_index(i: u8) -> Option<Day> { /* 0..=6 → variant */ }
pub fn to_index(self) -> u8 { self as u8 }
pub fn next_arithmetic(self) -> Day {
Day::from_index((self.to_index() + 1) % 7).unwrap()
}
}Comparison Table
| Aspect | OCaml | Rust | |||
|---|---|---|---|---|---|
| Declaration | `type day = Sun \ | Mon \ | ...` | `enum Day { Sun, Mon, ... }` | |
| Equality | Built-in (structural) | Requires `#[derive(PartialEq, Eq)]` | |||
| Copying | Implicit (all values copyable) | Requires `#[derive(Clone, Copy)]` | |||
| Debug printing | Via `ppx_deriving` or manual | `#[derive(Debug)]` | |||
| Pattern syntax | `function \ | Sun -> ...` | `match self { Day::Sun => ... }` | ||
| Or-patterns | `\ | Sun \ | Sat -> true` | `matches!(self, Day::Sun \ | Day::Sat)` |
| Namespace | Flat (just `Sun`) | Prefixed (`Day::Sun`) unless `use Day::*` | |||
| Method attachment | No (free functions only) | `impl Day { fn name(self) ... }` |
Type Signatures Explained
OCaml: `val day_name : day -> string` — simple function from variant to string Rust: `fn name(self) -> &'static str` — method taking `self` by copy (since `Day: Copy`), returning a string slice with `'static` lifetime (string literals live forever)
Takeaways
1. Near-identical concept: Both are sum types with exhaustive matching — the core idea transfers perfectly 2. Rust requires opt-in traits: `derive` macros replace OCaml's built-in structural equality and copy 3. Namespace difference: Rust variants are namespaced (`Day::Sun`); OCaml's are module-level (`Sun`) 4. Methods vs functions: Rust encourages `impl` blocks; OCaml keeps everything as standalone functions 5. `matches!` macro is Rust's ergonomic equivalent of OCaml's multi-arm pattern returning `bool`