๐Ÿฆ€ Functional Rust
๐ŸŽฌ Fearless Concurrency Threads, Arc>, channels โ€” safe parallelism enforced by the compiler.
๐Ÿ“ Text version (for readers / accessibility)

โ€ข std::thread::spawn creates OS threads โ€” closures must be Send + 'static

โ€ข Arc> provides shared mutable state across threads safely

โ€ข Channels (mpsc) enable message passing โ€” multiple producers, single consumer

โ€ข Send and Sync marker traits enforce thread safety at compile time

โ€ข Data races are impossible โ€” the type system prevents them before your code runs

Example 275: Yacht Dice Scoring

Difficulty: โญโญ Category: Pattern Matching | Algebraic Data Types OCaml Source: Classic Yacht dice game scoring exercise

Problem Statement

Score a roll of five dice against one of twelve Yacht categories. Each category has different rules: number categories sum matching dice, Yacht awards 50 for five-of-a-kind, FullHouse awards the sum when dice form a 2+3 pair, and straights award 30 for the sequences 1-5 or 2-6.

Learning Outcomes

OCaml Approach

OCaml uses a variant type for categories and dispatches with a multi-arm `function` expression. FullHouse is detected by matching sorted list patterns with guards, and FourOfAKind uses `List.find` with a `try/with Not_found` fallback. Sorted comparison against literal lists handles the straights.

Rust Approach

Rust models the same logic with a `#[derive]`-annotated enum and an exhaustive `match`. Fixed-size `[u8; 5]` arrays replace OCaml lists; `sort_unstable` replaces `List.sort`. FullHouse uses a frequency-count approach (more robust than pattern-matching on sorted values). FourOfAKind uses iterator `.find().map().unwrap_or(0)`, replacing the exception-based OCaml idiom cleanly.

Key Differences

1. Variant / enum: OCaml `type category = Ones | ...` maps directly to `pub enum Category { Ones, ... }` โ€” the concept is identical, the syntax is similar. 2. Fixed-length data: OCaml uses `'a list` for dice; Rust uses `[u8; 5]`, making the "exactly five dice" invariant a compile-time guarantee. 3. Error handling: OCaml `List.find` raises `Not_found`; Rust `Iterator::find` returns `Option`, handled with `.map().unwrap_or(0)` โ€” no exceptions. 4. FullHouse detection: OCaml matches sorted patterns; Rust builds a frequency table and checks sorted frequency counts equal `[2, 3]` โ€” avoids fragile guard expressions that clippy flags.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Category {
    Ones,
    Twos,
    Threes,
    Fours,
    Fives,
    Sixes,
    FullHouse,
    FourOfAKind,
    LittleStraight,
    BigStraight,
    Yacht,
    Choice,
}

fn count(dice: &[u8], n: u8) -> u8 {
    dice.iter().filter(|&&d| d == n).count() as u8
}

pub fn score(dice: &[u8; 5], category: Category) -> u32 {
    match category {
        Category::Ones => u32::from(count(dice, 1)),
        Category::Twos => 2 * u32::from(count(dice, 2)),
        Category::Threes => 3 * u32::from(count(dice, 3)),
        Category::Fours => 4 * u32::from(count(dice, 4)),
        Category::Fives => 5 * u32::from(count(dice, 5)),
        Category::Sixes => 6 * u32::from(count(dice, 6)),
        Category::Choice => dice.iter().map(|&d| u32::from(d)).sum(),
        Category::Yacht => {
            if dice.iter().all(|&d| d == dice[0]) {
                50
            } else {
                0
            }
        }
        Category::FullHouse => {
            let mut counts = [0u8; 7];
            for &d in dice {
                counts[d as usize] += 1;
            }
            let freqs: Vec<u8> = counts.iter().copied().filter(|&c| c > 0).collect();
            let mut sorted_freqs = freqs.clone();
            sorted_freqs.sort_unstable();
            if sorted_freqs == [2, 3] {
                dice.iter().map(|&d| u32::from(d)).sum()
            } else {
                0
            }
        }
        Category::FourOfAKind => (1u8..=6)
            .find(|&n| count(dice, n) >= 4)
            .map(|n| 4 * u32::from(n))
            .unwrap_or(0),
        Category::LittleStraight => {
            let mut sorted = *dice;
            sorted.sort_unstable();
            if sorted == [1, 2, 3, 4, 5] { 30 } else { 0 }
        }
        Category::BigStraight => {
            let mut sorted = *dice;
            sorted.sort_unstable();
            if sorted == [2, 3, 4, 5, 6] { 30 } else { 0 }
        }
    }
}

pub fn score_four_of_a_kind_recursive(dice: &[u8; 5], face: u8) -> u32 {
    if face > 6 {
        return 0;
    }
    if count(dice, face) >= 4 {
        4 * u32::from(face)
    } else {
        score_four_of_a_kind_recursive(dice, face + 1)
    }
}

fn main() {
    println!("Yacht [5,5,5,5,5] = {}", score(&[5, 5, 5, 5, 5], Category::Yacht));
    println!("FullHouse [2,2,3,3,3] = {}", score(&[2, 2, 3, 3, 3], Category::FullHouse));
    println!("Choice [1,2,3,4,5] = {}", score(&[1, 2, 3, 4, 5], Category::Choice));
    println!("LittleStraight [1,2,3,4,5] = {}", score(&[1, 2, 3, 4, 5], Category::LittleStraight));
    println!("BigStraight [2,3,4,5,6] = {}", score(&[2, 3, 4, 5, 6], Category::BigStraight));
    println!("FourOfAKind [3,3,3,3,1] = {}", score(&[3, 3, 3, 3, 1], Category::FourOfAKind));
    println!(
        "FourOfAKind recursive [2,2,2,2,5] = {}",
        score_four_of_a_kind_recursive(&[2, 2, 2, 2, 5], 1)
    );
}

/* Output:
   Yacht [5,5,5,5,5] = 50
   FullHouse [2,2,3,3,3] = 13
   Choice [1,2,3,4,5] = 15
   LittleStraight [1,2,3,4,5] = 30
   BigStraight [2,3,4,5,6] = 30
   FourOfAKind [3,3,3,3,1] = 12
   FourOfAKind recursive [2,2,2,2,5] = 8
*/
type category = Ones | Twos | Threes | Fours | Fives | Sixes
  | FullHouse | FourOfAKind | LittleStraight | BigStraight | Yacht | Choice

let count dice n = List.length (List.filter ((=) n) dice)

let score dice = function
  | Ones -> count dice 1 | Twos -> 2 * count dice 2
  | Threes -> 3 * count dice 3 | Fours -> 4 * count dice 4
  | Fives -> 5 * count dice 5 | Sixes -> 6 * count dice 6
  | Choice -> List.fold_left (+) 0 dice
  | Yacht -> if List.for_all ((=) (List.hd dice)) dice then 50 else 0
  | FullHouse ->
    let sorted = List.sort compare dice in
    (match sorted with
     | [a;b;c;d;e] when a=b && b=c && d=e && c<>d -> List.fold_left (+) 0 dice
     | [a;b;c;d;e] when a=b && c=d && d=e && b<>c -> List.fold_left (+) 0 dice
     | _ -> 0)
  | FourOfAKind ->
    (try
       let v = List.find (fun n -> count dice n >= 4) (List.sort_uniq compare dice) in
       4 * v
     with Not_found -> 0)
  | LittleStraight ->
    if List.sort compare dice = [1;2;3;4;5] then 30 else 0
  | BigStraight ->
    if List.sort compare dice = [2;3;4;5;6] then 30 else 0

let () =
  assert (score [5;5;5;5;5] Yacht = 50);
  assert (score [2;2;3;3;3] FullHouse = 13);
  assert (score [1;2;3;4;5] Choice = 15);
  assert (score [1;2;3;4;5] LittleStraight = 30);
  assert (score [2;3;4;5;6] BigStraight = 30);
  print_endline "ok"

๐Ÿ“Š Detailed Comparison

OCaml vs Rust: Yacht Dice Scoring

Side-by-Side Code

OCaml

๐Ÿช Show OCaml equivalent
type category = Ones | Twos | Threes | Fours | Fives | Sixes
| FullHouse | FourOfAKind | LittleStraight | BigStraight | Yacht | Choice

let count dice n = List.length (List.filter ((=) n) dice)

let score dice = function
| Ones   -> count dice 1 | Twos   -> 2 * count dice 2
| Threes -> 3 * count dice 3 | Fours  -> 4 * count dice 4
| Fives  -> 5 * count dice 5 | Sixes  -> 6 * count dice 6
| Choice -> List.fold_left (+) 0 dice
| Yacht  -> if List.for_all ((=) (List.hd dice)) dice then 50 else 0
| FullHouse ->
 let sorted = List.sort compare dice in
 (match sorted with
  | [a;b;c;d;e] when a=b && b=c && d=e && c<>d -> List.fold_left (+) 0 dice
  | [a;b;c;d;e] when a=b && c=d && d=e && b<>c -> List.fold_left (+) 0 dice
  | _ -> 0)
| FourOfAKind ->
 (try
    let v = List.find (fun n -> count dice n >= 4)
                      (List.sort_uniq compare dice) in
    4 * v
  with Not_found -> 0)
| LittleStraight ->
 if List.sort compare dice = [1;2;3;4;5] then 30 else 0
| BigStraight ->
 if List.sort compare dice = [2;3;4;5;6] then 30 else 0

Rust (idiomatic)

pub fn score(dice: &[u8; 5], category: Category) -> u32 {
 match category {
     Category::Ones   => u32::from(count(dice, 1)),
     Category::Twos   => 2 * u32::from(count(dice, 2)),
     Category::Threes => 3 * u32::from(count(dice, 3)),
     Category::Fours  => 4 * u32::from(count(dice, 4)),
     Category::Fives  => 5 * u32::from(count(dice, 5)),
     Category::Sixes  => 6 * u32::from(count(dice, 6)),
     Category::Choice => dice.iter().map(|&d| u32::from(d)).sum(),
     Category::Yacht  => {
         if dice.iter().all(|&d| d == dice[0]) { 50 } else { 0 }
     }
     Category::FullHouse => {
         let mut counts = [0u8; 7];
         for &d in dice { counts[d as usize] += 1; }
         let mut freqs: Vec<u8> = counts.iter().copied()
             .filter(|&c| c > 0).collect();
         freqs.sort_unstable();
         if freqs == [2, 3] { dice.iter().map(|&d| u32::from(d)).sum() }
         else { 0 }
     }
     Category::FourOfAKind => (1u8..=6)
         .find(|&n| count(dice, n) >= 4)
         .map(|n| 4 * u32::from(n))
         .unwrap_or(0),
     Category::LittleStraight => {
         let mut s = *dice; s.sort_unstable();
         if s == [1, 2, 3, 4, 5] { 30 } else { 0 }
     }
     Category::BigStraight => {
         let mut s = *dice; s.sort_unstable();
         if s == [2, 3, 4, 5, 6] { 30 } else { 0 }
     }
 }
}

Rust (functional/recursive โ€” FourOfAKind)

pub fn score_four_of_a_kind_recursive(dice: &[u8; 5], face: u8) -> u32 {
 if face > 6 { return 0; }
 if count(dice, face) >= 4 {
     4 * u32::from(face)
 } else {
     score_four_of_a_kind_recursive(dice, face + 1)
 }
}

Type Signatures

ConceptOCamlRust
Category type`type category = Ones \Twos \...``pub enum Category { Ones, Twos, ... }`
Dice parameter`'a list` (unbounded)`&[u8; 5]` (exactly 5 dice)
Score result`int``u32`
Count helper`dice -> int -> int``fn count(dice: &[u8], n: u8) -> u8`
Missing value`Not_found` exception`Option<T>` โ†’ `.unwrap_or(0)`

Key Insights

1. Enums are the same concept: OCaml variant types and Rust enums are both sum types encoding a closed set of alternatives. The translation is nearly mechanical โ€” rename and adjust syntax.

2. Fixed-length arrays enforce invariants: OCaml represents five dice as `'a list`; Rust uses `[u8; 5]`. The array type encodes "exactly five" at compile time, eliminating a class of runtime errors with no overhead.

3. Options replace exceptions: OCaml's `List.find` raises `Not_found` when nothing matches, caught with `try/with`. Rust's `Iterator::find` returns `Option<T>`. The `.find().map(f).unwrap_or(default)` chain is safer and composes without stack-unwinding overhead.

4. Frequency table beats sorted-pattern matching: OCaml's FullHouse implementation matches on two sorted list patterns with guards โ€” readable but fragile and clippy-hostile when ported to Rust. Building a frequency table (`counts[face] += 1`) and comparing sorted frequency counts to `[2, 3]` is cleaner, handles all orderings, and passes clippy without special cases.

5. `sort_unstable` vs `List.sort`: Rust's `sort_unstable` on a `[u8; 5]` stack array is zero-allocation and O(n log n). OCaml's `List.sort` allocates a sorted list. For five elements neither matters practically, but the Rust version has no heap traffic at all.

When to Use Each Style

Use idiomatic Rust when: Scoring real game logic โ€” the frequency-table approach for FullHouse and the iterator chain for FourOfAKind are easier to maintain and extend (e.g., adding validation).

Use recursive Rust when: Teaching the OCaml-to-Rust mental model โ€” the recursive `score_four_of_a_kind_recursive` mirrors the OCaml `List.find` tail-recursion pattern explicitly and shows how recursion replaces looping over a range.