Example 089: Poker Hand Evaluator
Difficulty: βββ Category: Records and Variants OCaml Source: Classic functional pattern-matching exerciseProblem Statement
Given five card ranks (as integers 2β14) and a boolean indicating whether all cards share the same suit, classify the poker hand into one of nine categories from High Card up to Straight Flush.Learning Outcomes
- Multi-arm tuple pattern matching: `match (is_flush, is_straight, counts.as_slice())`
- Slice patterns (`[4, ..]`, `[3, 2]`, `[2, 2, ..]`) for structural decomposition
- Deriving `PartialOrd`/`Ord` on enums to get free comparison ordering
- Using a HashMap inside a block expression to produce a sorted `Vec` in one binding
OCaml Approach
OCaml classifies the hand by first computing a descending list of rank-count frequencies (e.g., `[3; 2]` for a full house), then pattern-matching a triple `(is_flush, is_straight, counts)` against concrete list shapes. Because OCaml lists carry structural equality, `[3; 2]` matches exactly a full-house count list.Rust Approach
Rust mirrors the logic with a `Vec<usize>` of sorted counts passed as `counts.as_slice()` into a `match`. Slice patterns (`[4, ..]`, `[3, 2]`) replace OCaml list patterns one-to-one. The `HandType` enum derives `Ord` so hands are naturally comparable without a separate ranking function.Key Differences
1. Pattern subjects: OCaml matches `(bool, bool, int list)`; Rust matches `(bool, bool, &[usize])` using stable slice pattern syntax. 2. Structural list patterns: OCaml uses `4 :: _` for "starts with 4"; Rust uses `[4, ..]`. 3. Enum ordering: OCaml needs a separate comparison function; Rust `#[derive(Ord)]` gives ordering for free based on declaration order. 4. Frequency counting: OCaml threads `List.filter` through `List.map` on unique values; Rust uses a `HashMap` then converts to a sorted `Vec`.use std::collections::HashMap;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
pub enum HandType {
HighCard,
Pair,
TwoPair,
ThreeKind,
Straight,
Flush,
FullHouse,
FourKind,
StraightFlush,
}
impl HandType {
pub fn name(self) -> &'static str {
match self {
HandType::StraightFlush => "Straight Flush",
HandType::FourKind => "Four of a Kind",
HandType::FullHouse => "Full House",
HandType::Flush => "Flush",
HandType::Straight => "Straight",
HandType::ThreeKind => "Three of a Kind",
HandType::TwoPair => "Two Pair",
HandType::Pair => "Pair",
HandType::HighCard => "High Card",
}
}
}
// Solution 1: Idiomatic Rust β HashMap for counting, iterator for classification
pub fn classify(ranks: &[u8], is_flush: bool) -> HandType {
let counts: Vec<usize> = {
let mut map = HashMap::new();
for &r in ranks {
*map.entry(r).or_insert(0usize) += 1;
}
let mut v: Vec<usize> = map.into_values().collect();
v.sort_unstable_by(|a, b| b.cmp(a));
v
};
let mut sorted = ranks.to_vec();
sorted.sort_unstable_by(|a, b| b.cmp(a));
let is_straight = sorted.len() == 5
&& counts.iter().all(|&c| c == 1)
&& (sorted[0] as i32 - sorted[4] as i32) == 4;
match (is_flush, is_straight, counts.as_slice()) {
(true, true, _) => HandType::StraightFlush,
(_, _, [4, ..]) => HandType::FourKind,
(_, _, [3, 2]) => HandType::FullHouse,
(true, _, _) => HandType::Flush,
(_, true, _) => HandType::Straight,
(_, _, [3, ..]) => HandType::ThreeKind,
(_, _, [2, 2, ..]) => HandType::TwoPair,
(_, _, [2, ..]) => HandType::Pair,
_ => HandType::HighCard,
}
}
// Solution 2: Functional/recursive β mirrors OCaml's explicit recursion style
fn count_occurrences(rank: u8, ranks: &[u8]) -> usize {
ranks.iter().filter(|&&r| r == rank).count()
}
fn unique_sorted(ranks: &[u8]) -> Vec<u8> {
let mut seen: Vec<u8> = Vec::new();
for &r in ranks {
if !seen.contains(&r) {
seen.push(r);
}
}
seen.sort_unstable();
seen
}
pub fn classify_functional(ranks: &[u8], is_flush: bool) -> HandType {
let mut sorted = ranks.to_vec();
sorted.sort_unstable_by(|a, b| b.cmp(a));
let uniq = unique_sorted(&sorted);
let mut counts: Vec<usize> = uniq
.iter()
.map(|&r| count_occurrences(r, &sorted))
.collect();
counts.sort_unstable_by(|a, b| b.cmp(a));
let is_straight =
sorted.len() == 5 && uniq.len() == 5 && (sorted[0] as i32 - sorted[4] as i32) == 4;
match (is_flush, is_straight, counts.as_slice()) {
(true, true, _) => HandType::StraightFlush,
(_, _, [4, ..]) => HandType::FourKind,
(_, _, [3, 2]) => HandType::FullHouse,
(true, _, _) => HandType::Flush,
(_, true, _) => HandType::Straight,
(_, _, [3, ..]) => HandType::ThreeKind,
(_, _, [2, 2, ..]) => HandType::TwoPair,
(_, _, [2, ..]) => HandType::Pair,
_ => HandType::HighCard,
}
}
fn main() {
let hands: &[(&[u8], bool, &str)] = &[
(&[10, 11, 12, 13, 14], true, "Royal straight flush"),
(&[2, 3, 4, 5, 6], true, "Low straight flush"),
(&[9, 9, 9, 9, 5], false, "Four of a kind"),
(&[3, 3, 3, 7, 7], false, "Full house"),
(&[2, 5, 7, 9, 11], true, "Flush"),
(&[5, 6, 7, 8, 9], false, "Straight"),
(&[8, 8, 8, 3, 5], false, "Three of a kind"),
(&[4, 4, 7, 7, 9], false, "Two pair"),
(&[2, 2, 5, 8, 11], false, "Pair"),
(&[2, 5, 9, 11, 14], false, "High card"),
];
println!("Idiomatic classify:");
for (ranks, flush, label) in hands {
println!(" {:<25} β {}", label, classify(ranks, *flush).name());
}
println!("\nFunctional classify:");
for (ranks, flush, label) in hands {
println!(
" {:<25} β {}",
label,
classify_functional(ranks, *flush).name()
);
}
}
/* Output:
Idiomatic classify:
Royal straight flush β Straight Flush
Low straight flush β Straight Flush
Four of a kind β Four of a Kind
Full house β Full House
Flush β Flush
Straight β Straight
Three of a kind β Three of a Kind
Two pair β Two Pair
Pair β Pair
High card β High Card
Functional classify:
Royal straight flush β Straight Flush
Low straight flush β Straight Flush
Four of a kind β Four of a Kind
Full house β Full House
Flush β Flush
Straight β Straight
Three of a kind β Three of a Kind
Two pair β Two Pair
Pair β Pair
High card β High Card
*/
(* Poker Hand Evaluator β Complex Pattern Matching *)
type rank = int
type hand_type = HighCard | Pair | TwoPair | ThreeKind | Straight
| Flush | FullHouse | FourKind | StraightFlush
(* Idiomatic OCaml β classify a 5-card hand given ranks and flush status *)
let classify (ranks : rank list) (is_flush : bool) =
let sorted = List.sort (fun a b -> compare b a) ranks in
let counts = List.sort (fun a b -> compare b a)
(List.map (fun r -> List.length (List.filter ((=) r) sorted))
(List.sort_uniq compare sorted)) in
let is_straight = match sorted with
| [a;_;_;_;e] -> a - e = 4 && List.length (List.sort_uniq compare sorted) = 5
| _ -> false in
match is_flush, is_straight, counts with
| true, true, _ -> StraightFlush
| _, _, 4 :: _ -> FourKind
| _, _, [3; 2] -> FullHouse
| true, _, _ -> Flush
| _, true, _ -> Straight
| _, _, 3 :: _ -> ThreeKind
| _, _, [2; 2; 1] -> TwoPair
| _, _, 2 :: _ -> Pair
| _ -> HighCard
let name = function
| StraightFlush -> "Straight Flush" | FourKind -> "Four of a Kind"
| FullHouse -> "Full House" | Flush -> "Flush" | Straight -> "Straight"
| ThreeKind -> "Three of a Kind" | TwoPair -> "Two Pair"
| Pair -> "Pair" | HighCard -> "High Card"
(* Recursive helper β count occurrences of a value in a list *)
let rec count x = function
| [] -> 0
| h :: t -> (if h = x then 1 else 0) + count x t
(* Recursive version β builds counts by explicit recursion over unique ranks *)
let rec unique = function
| [] -> []
| h :: t -> h :: unique (List.filter ((<>) h) t)
let classify_recursive (ranks : rank list) (is_flush : bool) =
let sorted = List.sort (fun a b -> compare b a) ranks in
let uniq = List.sort compare (unique sorted) in
let counts = List.sort (fun a b -> compare b a) (List.map (fun r -> count r sorted) uniq) in
let is_straight = match sorted with
| [a;_;_;_;e] -> a - e = 4 && List.length uniq = 5
| _ -> false in
match is_flush, is_straight, counts with
| true, true, _ -> StraightFlush
| _, _, 4 :: _ -> FourKind
| _, _, [3; 2] -> FullHouse
| true, _, _ -> Flush
| _, true, _ -> Straight
| _, _, 3 :: _ -> ThreeKind
| _, _, [2; 2; 1] -> TwoPair
| _, _, 2 :: _ -> Pair
| _ -> HighCard
let () =
assert (classify [10;11;12;13;14] true = StraightFlush);
assert (classify [2;3;4;5;6] true = StraightFlush);
assert (classify [9;9;9;9;5] false = FourKind);
assert (classify [3;3;3;7;7] false = FullHouse);
assert (classify [2;5;7;9;11] true = Flush);
assert (classify [5;6;7;8;9] false = Straight);
assert (classify [8;8;8;3;5] false = ThreeKind);
assert (classify [4;4;7;7;9] false = TwoPair);
assert (classify [2;2;5;8;11] false = Pair);
assert (classify [2;5;9;11;14] false = HighCard);
assert (classify_recursive [3;3;3;7;7] false = FullHouse);
Printf.printf "%s\n" (name (classify [10;11;12;13;14] true));
Printf.printf "%s\n" (name (classify [3;3;3;7;7] false));
print_endline "ok"
π Detailed Comparison
OCaml vs Rust: Poker Hand Evaluator
Side-by-Side Code
OCaml
πͺ Show OCaml equivalent
type hand_type = HighCard | Pair | TwoPair | ThreeKind | Straight
| Flush | FullHouse | FourKind | StraightFlush
let classify (ranks : int list) (is_flush : bool) =
let sorted = List.sort (fun a b -> compare b a) ranks in
let counts = List.sort (fun a b -> compare b a)
(List.map (fun r -> List.length (List.filter ((=) r) sorted))
(List.sort_uniq compare sorted)) in
let is_straight = match sorted with
| [a;_;_;_;e] -> a - e = 4 && List.length (List.sort_uniq compare sorted) = 5
| _ -> false in
match is_flush, is_straight, counts with
| true, true, _ -> StraightFlush
| _, _, 4 :: _ -> FourKind
| _, _, [3; 2] -> FullHouse
| true, _, _ -> Flush
| _, true, _ -> Straight
| _, _, 3 :: _ -> ThreeKind
| _, _, [2; 2; 1] -> TwoPair
| _, _, 2 :: _ -> Pair
| _ -> HighCardRust (idiomatic)
pub fn classify(ranks: &[u8], is_flush: bool) -> HandType {
let counts: Vec<usize> = {
let mut map = HashMap::new();
for &r in ranks {
*map.entry(r).or_insert(0usize) += 1;
}
let mut v: Vec<usize> = map.into_values().collect();
v.sort_unstable_by(|a, b| b.cmp(a));
v
};
let mut sorted = ranks.to_vec();
sorted.sort_unstable_by(|a, b| b.cmp(a));
let is_straight = sorted.len() == 5
&& counts.iter().all(|&c| c == 1)
&& (sorted[0] as i32 - sorted[4] as i32) == 4;
match (is_flush, is_straight, counts.as_slice()) {
(true, true, _) => HandType::StraightFlush,
(_, _, [4, ..]) => HandType::FourKind,
(_, _, [3, 2]) => HandType::FullHouse,
(true, _, _) => HandType::Flush,
(_, true, _) => HandType::Straight,
(_, _, [3, ..]) => HandType::ThreeKind,
(_, _, [2, 2, ..]) => HandType::TwoPair,
(_, _, [2, ..]) => HandType::Pair,
_ => HandType::HighCard,
}
}Rust (functional/recursive)
fn count_occurrences(rank: u8, ranks: &[u8]) -> usize {
ranks.iter().filter(|&&r| r == rank).count()
}
fn unique_sorted(ranks: &[u8]) -> Vec<u8> {
let mut seen: Vec<u8> = Vec::new();
for &r in ranks {
if !seen.contains(&r) { seen.push(r); }
}
seen.sort_unstable();
seen
}
pub fn classify_functional(ranks: &[u8], is_flush: bool) -> HandType {
let mut sorted = ranks.to_vec();
sorted.sort_unstable_by(|a, b| b.cmp(a));
let uniq = unique_sorted(&sorted);
let mut counts: Vec<usize> = uniq
.iter()
.map(|&r| count_occurrences(r, &sorted))
.collect();
counts.sort_unstable_by(|a, b| b.cmp(a));
let is_straight = sorted.len() == 5
&& uniq.len() == 5
&& (sorted[0] as i32 - sorted[4] as i32) == 4;
// same match arm as above β¦
}Type Signatures
| Concept | OCaml | Rust |
|---|---|---|
| Classify function | `val classify : int list -> bool -> hand_type` | `fn classify(ranks: &[u8], is_flush: bool) -> HandType` |
| Rank sequence | `int list` | `&[u8]` (borrowed slice) |
| Hand category | `hand_type` (variant) | `HandType` (enum) |
| Count sequence | `int list` | `Vec<usize>` / `&[usize]` |
| List pattern | `4 :: _` | `[4, ..]` |
| Exact list pattern | `[3; 2]` | `[3, 2]` |
Key Insights
1. Slice patterns replace list patterns 1-to-1. OCaml's `4 :: _` becomes Rust's `[4, ..]`; OCaml's `[3; 2]` (exact two-element list) becomes Rust's `[3, 2]` (exact two-element slice). The mental model transfers directly.
2. Tuple patterns enable multi-axis dispatch. Both languages match a triple `(is_flush, is_straight, counts)` in one expression. Rust's exhaustiveness checker guarantees no case is missed, just like OCaml's.
3. Enum ordering is declared, not computed. OCaml needs a separate rank-comparison function to order hand types. In Rust, `#[derive(PartialOrd, Ord)]` gives ordering for free based on variant declaration order β HighCard first, StraightFlush last.
4. HashMap vs List.filter/map. OCaml computes frequencies with `List.filter ((=) r)` inside `List.map` β elegant but O(nΒ²). Rust's idiomatic solution uses a `HashMap` for O(n) counting. The functional Rust solution mirrors OCaml's approach explicitly for pedagogical clarity.
5. Owned vs borrowed counts. OCaml's garbage collector manages the intermediate lists freely. Rust builds counts into an owned `Vec<usize>`, then borrows it as `&[usize]` for the match β the block-expression pattern (`let v = { β¦ v }`) keeps the borrow lifetime clear.
When to Use Each Style
Use idiomatic Rust when: You want O(n) counting via HashMap and the clearest connection between algorithm and Rust idioms. Use functional/recursive Rust when: Teaching the OCamlβRust translation β the `count_occurrences` + `unique_sorted` decomposition mirrors OCaml's `List.filter` + `List.map` pipeline step by step.