ExamplesBy LevelBy TopicLearning Paths
953 Intermediate

953 Poker Hand Evaluator

Functional Programming

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

  • β€’ Build a rank-frequency map with HashMap and extract sorted count values
  • β€’ Detect a straight using counts.iter().all(|&c| c == 1) and a consecutive rank spread of 4
  • β€’ Detect a flush by checking that all cards share the same suit
  • β€’ Classify hand types by pattern-matching on (is_flush, is_straight, counts.as_slice())
  • β€’ Implement the same classification recursively using filter + count without a HashMap
  • Code 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

    AspectRustOCaml
    Frequency mapHashMap 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 orderADT comparison by declaration order
    Descending sortsort_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

    ConceptOCamlRust
    Classify functionval classify : int list -> bool -> hand_typefn classify(ranks: &[u8], is_flush: bool) -> HandType
    Rank sequenceint list&[u8] (borrowed slice)
    Hand categoryhand_type (variant)HandType (enum)
    Count sequenceint listVec<usize> / &[usize]
    List pattern4 :: _[4, ..]
    Exact list pattern[3; 2][3, 2]

    Key Insights

  • 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.
  • 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.
  • 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.
  • 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.
  • 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.

    Exercises

  • Add ace-low straight detection (A-2-3-4-5) where ace can count as 1.
  • Implement hand comparison: given two hands, determine which beats the other (handle ties with high-card comparison).
  • Extend to parse hand strings like "AS KH QD JC 10S" into (ranks, suits) before calling classify.
  • Implement a best_hand(hands: &[&str]) function that returns the index of the winning hand.
  • Add joker support: one wild card can substitute for any rank to maximize hand type.
  • Open Source Repos