๐Ÿฆ€ Functional Rust

1030: Group Elements by Key

Difficulty: Beginner Category: Data Structures Concept: Building a multimap (key โ†’ list of values) via the group-by pattern Key Insight: Rust's `entry().or_default().push()` is the idiomatic one-liner for group-by, replacing the verbose find-or-create-then-append pattern needed in OCaml.
// 1030: Group Elements by Key โ€” HashMap<K, Vec<V>>
// The classic group-by pattern using Entry API

use std::collections::HashMap;

/// Group words by their first character
fn group_by_first_letter() {
    let words = vec!["apple", "avocado", "banana", "blueberry", "cherry"];

    let mut groups: HashMap<char, Vec<&str>> = HashMap::new();
    for word in &words {
        let key = word.chars().next().unwrap();
        groups.entry(key).or_default().push(word);
    }

    assert_eq!(groups[&'a'], vec!["apple", "avocado"]);
    assert_eq!(groups[&'b'], vec!["banana", "blueberry"]);
    assert_eq!(groups[&'c'], vec!["cherry"]);
}

/// Group numbers by parity
fn group_by_parity() {
    let nums = vec![1, 2, 3, 4, 5, 6, 7, 8];

    let mut groups: HashMap<&str, Vec<i32>> = HashMap::new();
    for &n in &nums {
        let key = if n % 2 == 0 { "even" } else { "odd" };
        groups.entry(key).or_default().push(n);
    }

    assert_eq!(groups["even"], vec![2, 4, 6, 8]);
    assert_eq!(groups["odd"], vec![1, 3, 5, 7]);
}

/// Generic group_by function
fn group_by<T, K, F>(items: &[T], key_fn: F) -> HashMap<K, Vec<&T>>
where
    K: std::hash::Hash + Eq,
    F: Fn(&T) -> K,
{
    let mut groups: HashMap<K, Vec<&T>> = HashMap::new();
    for item in items {
        groups.entry(key_fn(item)).or_default().push(item);
    }
    groups
}

fn test_generic_group_by() {
    let data = vec![("Alice", 90), ("Bob", 85), ("Alice", 92), ("Bob", 88)];
    let groups = group_by(&data, |&(name, _)| name);

    assert_eq!(groups["Alice"].len(), 2);
    assert_eq!(groups["Bob"].len(), 2);
    assert_eq!(groups["Alice"][0].1, 90);
    assert_eq!(groups["Alice"][1].1, 92);
}

fn main() {
    group_by_first_letter();
    group_by_parity();
    test_generic_group_by();
    println!("โœ“ All tests passed");
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_group_by_first_letter() { group_by_first_letter(); }

    #[test]
    fn test_group_by_parity() { group_by_parity(); }

    #[test]
    fn test_generic() { test_generic_group_by(); }

    #[test]
    fn test_group_by_length() {
        let words = vec!["hi", "hey", "hello", "yo", "yes"];
        let groups = group_by(&words, |w| w.len());
        assert_eq!(groups[&2].len(), 2); // "hi", "yo"
        assert_eq!(groups[&3].len(), 2); // "hey", "yes"
        assert_eq!(groups[&5].len(), 1); // "hello"
    }
}
(* 1030: Group Elements by Key *)
(* Build a map from key to list of values *)

module StringMap = Map.Make(String)

(* Approach 1: Group words by first letter *)
let group_by_first_letter () =
  let words = ["apple"; "avocado"; "banana"; "blueberry"; "cherry"] in
  let groups = List.fold_left (fun acc w ->
    let key = String.make 1 w.[0] in
    let current = match StringMap.find_opt key acc with
      | Some lst -> lst
      | None -> []
    in
    StringMap.add key (current @ [w]) acc
  ) StringMap.empty words in
  assert (StringMap.find "a" groups = ["apple"; "avocado"]);
  assert (StringMap.find "b" groups = ["banana"; "blueberry"]);
  assert (StringMap.find "c" groups = ["cherry"])

(* Approach 2: Group numbers by property *)
let group_by_parity () =
  let nums = [1; 2; 3; 4; 5; 6; 7; 8] in
  let groups = List.fold_left (fun acc n ->
    let key = if n mod 2 = 0 then "even" else "odd" in
    let current = match StringMap.find_opt key acc with
      | Some lst -> lst
      | None -> []
    in
    StringMap.add key (current @ [n]) acc
  ) StringMap.empty nums in
  assert (StringMap.find "even" groups = [2; 4; 6; 8]);
  assert (StringMap.find "odd" groups = [1; 3; 5; 7])

(* Approach 3: Generic group_by function *)
let group_by (type a) key_fn (items : a list) =
  List.fold_left (fun acc item ->
    let key = key_fn item in
    let current = match StringMap.find_opt key acc with
      | Some lst -> lst
      | None -> []
    in
    StringMap.add key (current @ [item]) acc
  ) StringMap.empty items

let test_generic_group_by () =
  let data = [("Alice", 90); ("Bob", 85); ("Alice", 92); ("Bob", 88)] in
  let groups = group_by fst data in
  assert (StringMap.find "Alice" groups = [("Alice", 90); ("Alice", 92)]);
  assert (StringMap.find "Bob" groups = [("Bob", 85); ("Bob", 88)])

let () =
  group_by_first_letter ();
  group_by_parity ();
  test_generic_group_by ();
  Printf.printf "โœ“ All tests passed\n"

๐Ÿ“Š Detailed Comparison

Group Elements by Key โ€” Comparison

Core Insight

Group-by is one of the most common data transformation patterns. Both languages build a map from key to collection of values, but Rust's Entry API makes the pattern a one-liner while OCaml requires explicit option matching.

OCaml Approach

  • `find_opt` + pattern match + `add` with appended list
  • `List.fold_left` to accumulate groups
  • Appending to list end (`@ [item]`) is O(n) โ€” use cons + reverse for performance
  • Generic version uses first-class modules or functors for key type

Rust Approach

  • `entry(key).or_default().push(value)` โ€” single expressive line
  • `or_default()` creates empty `Vec` if key absent
  • Returns `&mut Vec<V>` so `push` works inline
  • Generic version needs `Hash + Eq` bounds on key type

Comparison Table

AspectOCamlRust
Core pattern`find_opt` + match + `add``entry().or_default().push()`
Lines of code4-5 per group-by1
Key constraint`Ord` (for `Map`)`Hash + Eq` (for `HashMap`)
Value collectionList (prepend is O(1))Vec (append is amortized O(1))
MutabilityReturns new map each stepMutates in place
Iterator method`fold_left`for loop or `fold`