953 Poker Hand Evaluator
Tutorial
The Problem
Classify a five-card poker hand (straight flush, four of a kind, full house, etc.) using functional techniques. Build a frequency map of rank counts, detect flush and straight conditions, then use a pattern match on the sorted count vector to classify the hand. Implement both a HashMap-based idiomatic version and a purely functional recursive alternative.
🎯 Learning Outcomes
HashMap and extract sorted count valuescounts.iter().all(|&c| c == 1) and a consecutive rank spread of 4(is_flush, is_straight, counts.as_slice())filter + count without a HashMapCode Example
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,
}
}Key Differences
| Aspect | Rust | OCaml |
|---|---|---|
| Frequency map | HashMap with entry().or_insert(0) | List.filter + List.length per rank |
| Slice patterns | [4, ..], [3, 2], etc. | [4; _], [3; 2], etc. |
| Enum ordering | #[derive(PartialOrd, Ord)] by declaration order | ADT comparison by declaration order |
| Descending sort | sort_unstable_by(\|a, b\| b.cmp(a)) | List.sort (fun a b -> compare b a) |
The pattern-match-on-counts approach is elegant and easily extended. Adding new hand variants (e.g., five-of-a-kind for wild cards) requires only a new enum variant and a new match arm.
OCaml Approach
type hand_type =
| HighCard | Pair | TwoPair | ThreeKind | Straight
| Flush | FullHouse | FourKind | StraightFlush
let classify ranks is_flush =
let counts =
List.sort_uniq compare ranks
|> List.map (fun r -> List.length (List.filter ((=) r) ranks))
|> List.sort (fun a b -> compare b a) (* descending *)
in
let sorted = List.sort (fun a b -> compare b a) ranks in
let is_straight =
List.for_all (fun c -> c = 1) counts &&
(List.hd sorted - List.nth sorted 4) = 4
in
match is_flush, is_straight, counts with
| true, true, _ -> StraightFlush
| _, _, [4; _] -> FourKind
| _, _, [3; 2] -> FullHouse
| true, _, _ -> Flush
| _, true, _ -> Straight
| _, _, [3; _] -> ThreeKind
| _, _, [2; 2; _] -> TwoPair
| _, _, [2; _] -> Pair
| _ -> HighCard
OCaml's pattern match on (is_flush, is_straight, counts) is structurally identical to the Rust version. The main difference is that OCaml's ADT constructors are ordered by declaration for comparison, just like Rust's #[derive(Ord)].
Full Source
#![allow(clippy::all)]
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,
}
}
/* 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
*/Deep Comparison
OCaml vs Rust: Poker Hand Evaluator
Side-by-Side Code
OCaml
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
| _ -> HighCard
Rust (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
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.(is_flush, is_straight, counts) in one expression. Rust's exhaustiveness checker guarantees no case is missed, just like OCaml's.#[derive(PartialOrd, Ord)] gives ordering for free based on variant declaration order β HighCard first, StraightFlush last.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.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.
Exercises
"AS KH QD JC 10S" into (ranks, suits) before calling classify.best_hand(hands: &[&str]) function that returns the index of the winning hand.