🦀 Functional Rust

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

Key Differences

ConceptOCamlRust
Declaration`type day = Sun \Mon \...``enum Day { Sun, Mon, ... }`
Debug printingAutomatic`#[derive(Debug)]` required
EqualityAutomatic`#[derive(PartialEq, Eq)]` required
Copy semanticsAutomatic`#[derive(Clone, Copy)]` required
MethodsFree functions`impl Day { fn name(self) ... }`
DisplayManual 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

AspectOCamlRust
Declaration`type day = Sun \Mon \...``enum Day { Sun, Mon, ... }`
EqualityBuilt-in (structural)Requires `#[derive(PartialEq, Eq)]`
CopyingImplicit (all values copyable)Requires `#[derive(Clone, Copy)]`
Debug printingVia `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)`
NamespaceFlat (just `Sun`)Prefixed (`Day::Sun`) unless `use Day::*`
Method attachmentNo (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`