1041-multimap — Multimap
Tutorial
The Problem
A multimap (or multi-dictionary) maps each key to multiple values. Real-world examples: a person can have multiple phone numbers, a web server can handle multiple routes for the same path prefix, a product can belong to multiple categories. Standard hash maps do not support this — inserting the same key twice overwrites the old value.
The canonical Rust implementation uses HashMap<K, Vec<V>> as the underlying storage. This example wraps it in a MultiMap<K, V> struct providing ergonomic insert, get (returning a slice), and count operations.
🎯 Learning Outcomes
MultiMap<K, V> using HashMap<K, Vec<V>>get(&key) -> &[V] returning a slice for zero-copy accessmultimap crate variants are preferred over custom implementationsCode Example
#![allow(clippy::all)]
// 1041: Multimap — HashMap<K, Vec<V>> with Helpers
// A map where each key can have multiple values
use std::collections::HashMap;
/// A multimap: each key maps to a Vec of values
struct MultiMap<K, V> {
inner: HashMap<K, Vec<V>>,
}
impl<K: std::hash::Hash + Eq, V> MultiMap<K, V> {
fn new() -> Self {
MultiMap {
inner: HashMap::new(),
}
}
fn insert(&mut self, key: K, value: V) {
self.inner.entry(key).or_default().push(value);
}
fn get(&self, key: &K) -> &[V] {
self.inner.get(key).map_or(&[], |v| v.as_slice())
}
fn remove_key(&mut self, key: &K) -> Option<Vec<V>> {
self.inner.remove(key)
}
fn count_values(&self, key: &K) -> usize {
self.get(key).len()
}
fn total_values(&self) -> usize {
self.inner.values().map(|v| v.len()).sum()
}
fn keys(&self) -> impl Iterator<Item = &K> {
self.inner.keys()
}
fn contains_key(&self, key: &K) -> bool {
self.inner.contains_key(key)
}
}
impl<K: std::hash::Hash + Eq, V: PartialEq> MultiMap<K, V> {
fn remove_value(&mut self, key: &K, value: &V) -> bool {
if let Some(values) = self.inner.get_mut(key) {
if let Some(pos) = values.iter().position(|v| v == value) {
values.remove(pos);
if values.is_empty() {
self.inner.remove(key);
}
return true;
}
}
false
}
fn contains_value(&self, key: &K, value: &V) -> bool {
self.get(key).contains(value)
}
}
fn basic_ops() {
let mut mm = MultiMap::new();
mm.insert("fruits", "apple");
mm.insert("fruits", "banana");
mm.insert("fruits", "cherry");
mm.insert("vegs", "carrot");
mm.insert("vegs", "pea");
assert_eq!(mm.get(&"fruits"), &["apple", "banana", "cherry"]);
assert_eq!(mm.get(&"vegs"), &["carrot", "pea"]);
assert_eq!(mm.count_values(&"fruits"), 3);
assert_eq!(mm.total_values(), 5);
}
fn remove_ops() {
let mut mm = MultiMap::new();
mm.insert("tags", "rust");
mm.insert("tags", "ocaml");
mm.insert("tags", "haskell");
assert!(mm.remove_value(&"tags", &"ocaml"));
assert_eq!(mm.get(&"tags"), &["rust", "haskell"]);
mm.remove_key(&"tags");
assert_eq!(mm.get(&"tags"), &[] as &[&str]);
}
fn build_index() {
let data = vec![
("lang", "Rust"),
("lang", "OCaml"),
("lang", "Haskell"),
("paradigm", "functional"),
("paradigm", "imperative"),
];
let mut mm = MultiMap::new();
for (key, value) in data {
mm.insert(key, value);
}
assert_eq!(mm.get(&"lang"), &["Rust", "OCaml", "Haskell"]);
assert_eq!(mm.get(&"paradigm"), &["functional", "imperative"]);
assert!(mm.contains_value(&"lang", &"Rust"));
assert!(!mm.contains_value(&"lang", &"Python"));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic() {
basic_ops();
}
#[test]
fn test_remove() {
remove_ops();
}
#[test]
fn test_index() {
build_index();
}
#[test]
fn test_empty_get() {
let mm: MultiMap<&str, i32> = MultiMap::new();
assert_eq!(mm.get(&"missing"), &[] as &[i32]);
assert_eq!(mm.count_values(&"missing"), 0);
}
}Key Differences
HashMap<K, Vec<V>> mutates in place.get returns &[V] — a zero-copy view; OCaml's find returns the list by value.or_default().push() is one lookup; OCaml requires two operations (find + add).multimap crate and indexmap::IndexMap provide production-ready multimaps; OCaml's Base.Map.add_multi is the stdlib equivalent.OCaml Approach
OCaml's functional multimap uses a persistent map of lists:
module StringMultiMap = Map.Make(String)
let insert key value m =
let values = try StringMultiMap.find key m with Not_found -> [] in
StringMultiMap.add key (value :: values) m
let get key m =
try StringMultiMap.find key m with Not_found -> []
Each insert creates a new map version. The Base.Map.add_multi function provides this in one call.
Full Source
#![allow(clippy::all)]
// 1041: Multimap — HashMap<K, Vec<V>> with Helpers
// A map where each key can have multiple values
use std::collections::HashMap;
/// A multimap: each key maps to a Vec of values
struct MultiMap<K, V> {
inner: HashMap<K, Vec<V>>,
}
impl<K: std::hash::Hash + Eq, V> MultiMap<K, V> {
fn new() -> Self {
MultiMap {
inner: HashMap::new(),
}
}
fn insert(&mut self, key: K, value: V) {
self.inner.entry(key).or_default().push(value);
}
fn get(&self, key: &K) -> &[V] {
self.inner.get(key).map_or(&[], |v| v.as_slice())
}
fn remove_key(&mut self, key: &K) -> Option<Vec<V>> {
self.inner.remove(key)
}
fn count_values(&self, key: &K) -> usize {
self.get(key).len()
}
fn total_values(&self) -> usize {
self.inner.values().map(|v| v.len()).sum()
}
fn keys(&self) -> impl Iterator<Item = &K> {
self.inner.keys()
}
fn contains_key(&self, key: &K) -> bool {
self.inner.contains_key(key)
}
}
impl<K: std::hash::Hash + Eq, V: PartialEq> MultiMap<K, V> {
fn remove_value(&mut self, key: &K, value: &V) -> bool {
if let Some(values) = self.inner.get_mut(key) {
if let Some(pos) = values.iter().position(|v| v == value) {
values.remove(pos);
if values.is_empty() {
self.inner.remove(key);
}
return true;
}
}
false
}
fn contains_value(&self, key: &K, value: &V) -> bool {
self.get(key).contains(value)
}
}
fn basic_ops() {
let mut mm = MultiMap::new();
mm.insert("fruits", "apple");
mm.insert("fruits", "banana");
mm.insert("fruits", "cherry");
mm.insert("vegs", "carrot");
mm.insert("vegs", "pea");
assert_eq!(mm.get(&"fruits"), &["apple", "banana", "cherry"]);
assert_eq!(mm.get(&"vegs"), &["carrot", "pea"]);
assert_eq!(mm.count_values(&"fruits"), 3);
assert_eq!(mm.total_values(), 5);
}
fn remove_ops() {
let mut mm = MultiMap::new();
mm.insert("tags", "rust");
mm.insert("tags", "ocaml");
mm.insert("tags", "haskell");
assert!(mm.remove_value(&"tags", &"ocaml"));
assert_eq!(mm.get(&"tags"), &["rust", "haskell"]);
mm.remove_key(&"tags");
assert_eq!(mm.get(&"tags"), &[] as &[&str]);
}
fn build_index() {
let data = vec![
("lang", "Rust"),
("lang", "OCaml"),
("lang", "Haskell"),
("paradigm", "functional"),
("paradigm", "imperative"),
];
let mut mm = MultiMap::new();
for (key, value) in data {
mm.insert(key, value);
}
assert_eq!(mm.get(&"lang"), &["Rust", "OCaml", "Haskell"]);
assert_eq!(mm.get(&"paradigm"), &["functional", "imperative"]);
assert!(mm.contains_value(&"lang", &"Rust"));
assert!(!mm.contains_value(&"lang", &"Python"));
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic() {
basic_ops();
}
#[test]
fn test_remove() {
remove_ops();
}
#[test]
fn test_index() {
build_index();
}
#[test]
fn test_empty_get() {
let mm: MultiMap<&str, i32> = MultiMap::new();
assert_eq!(mm.get(&"missing"), &[] as &[i32]);
assert_eq!(mm.count_values(&"missing"), 0);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic() {
basic_ops();
}
#[test]
fn test_remove() {
remove_ops();
}
#[test]
fn test_index() {
build_index();
}
#[test]
fn test_empty_get() {
let mm: MultiMap<&str, i32> = MultiMap::new();
assert_eq!(mm.get(&"missing"), &[] as &[i32]);
assert_eq!(mm.count_values(&"missing"), 0);
}
}
Deep Comparison
Multimap — Comparison
Core Insight
A multimap (one key → many values) is a thin wrapper over a map-to-list/vec. Both languages build it the same way — the difference is ergonomics. Rust's Entry API makes insertion clean; OCaml requires explicit find-or-create.
OCaml Approach
'a list StringMap.t — map to list of valuesadd: find_opt + append + addremove_value: filter + conditional removeRust Approach
HashMap<K, Vec<V>> wrapped in a structentry().or_default().push() for insertionremove_value: find + position + removeV: PartialEq operationsmap_or(&[], ...) for safe empty accessComparison Table
| Feature | OCaml | Rust |
|---|---|---|
| Type | 'a list Map.t | HashMap<K, Vec<V>> |
| Insert | find_opt + append + add | entry().or_default().push() |
| Get | find_opt / default [] | get().map_or(&[], ...) |
| Remove value | filter + conditional remove | position + remove |
| Mutability | Immutable | Mutable |
| Trait bounds | Ord (Map functor) | Hash + Eq (HashMap) |
Exercises
remove_value(&mut self, key: &K, value: &V) that removes one specific value from a key, keeping other values.values_flat(&self) -> impl Iterator<Item=&V> that iterates all values across all keys.invert<K, V>(m: &MultiMap<K, V>) -> MultiMap<V, K> that inverts the multimap, mapping each value back to its keys.