ExamplesBy LevelBy TopicLearning Paths
058 Fundamental

Variants — Days of the Week

Algebraic Data Types

Tutorial Video

Text description (accessibility)

This video demonstrates the "Variants — Days of the Week" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Algebraic Data Types. Model the days of the week as an algebraic data type. Key difference from OCaml: 1. **Derive macros:** Rust requires explicit `#[derive(Debug, Clone, Copy, PartialEq, Eq)]`; OCaml gets these implicitly

Tutorial

The Problem

Model the days of the week as an algebraic data type. Implement day_name, is_weekend, and next_day using exhaustive pattern matching.

🎯 Learning Outcomes

  • • Map OCaml variants to Rust enums (fieldless / C-like)
  • • See how both languages enforce exhaustive pattern matching
  • • Compare OCaml's free functions with Rust's impl methods
  • • Use #[derive] to get traits OCaml variants have implicitly
  • • Explore numeric discriminants as an alternative to match
  • 🦀 The Rust Way

  • Idiomatic: Enum with impl block methods (self.name(), self.is_weekend(), self.next())
  • Functional: Free functions mirroring OCaml's style
  • Numeric: Using discriminant values and arithmetic for next_day
  • Code Example

    #[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,
            }
        }
    }

    Key Differences

  • Derive macros: Rust requires explicit #[derive(Debug, Clone, Copy, PartialEq, Eq)]; OCaml gets these implicitly
  • Methods vs functions: Rust idiom puts functions in impl blocks; OCaml uses standalone functions
  • Display: Rust needs explicit Display impl; OCaml can derive show via ppx or manually print
  • Copy semantics: Copy must be opted into in Rust; OCaml values are always copyable
  • Exhaustiveness: Both compilers warn on non-exhaustive matches — this is a shared strength
  • Enum as closed set: Rust's enum Day and OCaml's type day = Mon | Tue | ... are closed sum types — the set of values is fixed at definition time. This enables exhaustiveness checking.
  • **Pattern matching vs switch:** Both languages check exhaustiveness at compile time. Java's switch on enums checks exhaustiveness only with --enable-preview in Java 17+. C's switch has no exhaustiveness checking.
  • **Deriving PartialEq, Debug:** Rust's #[derive] automatically generates equality and debug printing for enums. OCaml's structural equality (=) works on variant types without any annotation.
  • Methods on enums: Rust's impl Day block adds methods directly to the enum. OCaml uses standalone functions (no method syntax). This is a major ergonomic difference.
  • OCaml Approach

    OCaml variants are lightweight — declare the type, write functions with match/function. Equality, printing, and copying come free. All functions are standalone (no method syntax).

    Full Source

    #![allow(clippy::all)]
    //! # 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");
        }
    }
    ✓ Tests Rust test suite
    #[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");
        }
    }

    Deep Comparison

    Comparison: Variants — Days of the Week — OCaml vs Rust

    OCaml

    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
    Declarationtype 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 syntaxfunction \| Sun -> ...match self { Day::Sun => ... }
    Or-patterns\| Sun \| Sat -> truematches!(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

  • Near-identical concept: Both are sum types with exhaustive matching — the core idea transfers perfectly
  • Rust requires opt-in traits: derive macros replace OCaml's built-in structural equality and copy
  • Namespace difference: Rust variants are namespaced (Day::Sun); OCaml's are module-level (Sun)
  • Methods vs functions: Rust encourages impl blocks; OCaml keeps everything as standalone functions
  • **matches! macro** is Rust's ergonomic equivalent of OCaml's multi-arm pattern returning bool
  • Exercises

  • Add a method is_weekend to the Day enum using match, and implement working_days_until that counts weekdays between two days of the week.
  • Extend the Day enum to a WorkDay { day: Day, hours: f32 } struct and implement total_hours for a slice of WorkDay values using an iterator fold.
  • Define a Month enum with all 12 months and implement days_in_month that accounts for leap years, then write a calendar_days iterator that yields every (Month, u8) day pair for a given year.
  • Day arithmetic: Implement days_until(from: Day, to: Day) -> usize that returns the number of days from from to to, wrapping around (Monday to Friday = 4, Friday to Monday = 3).
  • Business days: Implement is_business_day(day: Day) -> bool and next_business_day(day: Day) -> Day using pattern matching. Then implement business_days_between(from: Day, to: Day) -> usize.
  • Open Source Repos