064: Map.Make Functor — String→Int Dictionary
Difficulty: ⭐⭐ Level: Intermediate How OCaml's module-level "functors" generate type-safe dictionary types — and what Rust's generics achieve instead.The Problem This Solves
You want a dictionary (key → value map) where keys are strings and values are integers. Simple. But then you need another map where keys must be sorted differently, or keys are a custom type. In many languages you end up copy-pasting your map implementation and tweaking it. OCaml solved this at the module level: `Map.Make(String)` is a function that takes a module (describing your key type and how to compare it) and produces an entire, specialized, type-safe `Map` module. This is OCaml's "functor" — a function from modules to modules. It's a different use of the word "functor" than the `Option::map` kind. In Rust, this same problem is solved by generics with trait bounds:// OCaml: module StringMap = Map.Make(String)
// Rust: BTreeMap<String, V> — the key type is a generic parameter
Rust's generics let you say "this collection works for any key type `K` that implements `Ord`" — which covers the same ground as OCaml's `Map.Make`, just with different syntax. The concept exists because "I need a sorted, type-safe dictionary parameterized by key type" is a fundamental need that both languages must address.
The Intuition
OCaml's `Map.Make` works like a cookie cutter factory. You hand it a cookie-cutter shape (a module that says "here's my key type and how to compare two keys"), and it stamps out a complete, ready-to-use map module tailored to that shape. Every cookie (map operation) it produces is type-safe for that exact key type. Rust achieves the same result differently: instead of generating a specialized module, it uses a single generic `BTreeMap<K, V>` that works for any `K: Ord`. The compiler specializes it at compile time for each concrete key type you use — effectively doing what `Map.Make` does, just implicitly. The end result is the same: a sorted, type-safe, immutable-friendly map. The mechanism differs:- OCaml: explicit module-level parameterization (the "functor" pattern)
- Rust: generic type parameters with trait bounds
How It Works in Rust
Step 1 — Build a word-length dictionary:use std::collections::BTreeMap;
// BTreeMap = sorted by key (like OCaml's Map.Make)
// HashMap = faster lookup, but unordered
fn word_lengths(words: &[&str]) -> BTreeMap<String, usize> {
words.iter()
.map(|w| (w.to_string(), w.len()))
.collect() // iterator of (key, value) pairs → BTreeMap
}
Step 2 — The map operations OCaml gives you for free:
// Filter by value (like OCaml's Map.filter)
fn filter_by_value<K: Ord + Clone, V: Clone>(
map: &BTreeMap<K, V>,
pred: impl Fn(&V) -> bool,
) -> BTreeMap<K, V> {
map.iter()
.filter(|(_, v)| pred(v))
.map(|(k, v)| (k.clone(), v.clone()))
.collect()
}
// Transform values (like OCaml's Map.map — a functor-style operation!)
fn map_values<K: Ord + Clone, V, U>(
map: &BTreeMap<K, V>,
f: impl Fn(&V) -> U,
) -> BTreeMap<K, U> {
map.iter()
.map(|(k, v)| (k.clone(), f(v)))
.collect()
}
Notice that `map_values` is a functor-style operation: it transforms the values inside the map without changing the keys or structure. The map is a container; `map_values` maps over its contents.
Step 3 — BTreeMap preserves insertion-independent ordering:
let words = vec!["zebra", "apple", "mango"];
let m = word_lengths(&words);
let keys: Vec<_> = m.keys().collect();
assert_eq!(keys, vec!["apple", "mango", "zebra"]); // always sorted
OCaml's `Map.Make` produces a balanced BST with the same property. Rust's `BTreeMap` is a B-tree with the same guarantee.
What This Unlocks
- Type-safe, sorted dictionaries with zero boilerplate. `BTreeMap<String, V>` handles any value type; the compiler enforces that keys implement `Ord`.
- Composable map transformations. `map_values`, `filter_by_value`, and similar functions compose over any `BTreeMap<K, V>` — generic code over all dictionary types at once.
- Ordered iteration for free. Unlike `HashMap`, `BTreeMap` always iterates in key order — useful for rendering sorted output, binary searching, range queries.
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| Creating a typed map module | `module StringMap = Map.Make(String)` — module-level functor | `BTreeMap<String, V>` — generic type parameter |
| What "functor" means here | A function from modules to modules (parameterized module) | Not called a functor — it's just generics |
| Key ordering requirement | Provided by the module argument (must have `compare`) | `K: Ord` trait bound |
| Sorted map type | `Map.Make(String).t` | `BTreeMap<K, V>` |
| Unordered/hash map | `Hashtbl` (mutable) | `HashMap<K, V>` |
| Immutability | Maps are persistent/immutable by default | Controlled by `mut` keyword; maps are mutable by default |
| `map` over values | `StringMap.map f m` | `map_values(&m, f)` (not built-in, but trivial via iterators) |
| Lookup | `StringMap.find_opt key m` → `option` | `m.get(key)` → `Option<&V>` |
/// Map.Make Functor — String→Int Dictionary
///
/// OCaml's `Map.Make(String)` creates a specialized balanced BST map.
/// Rust's `std::collections::BTreeMap` and `HashMap` serve the same role.
/// The key difference: OCaml uses functors (module-level functions) to
/// parameterize the map by key type; Rust uses generics with trait bounds.
use std::collections::BTreeMap;
/// Build a word-length map using BTreeMap (ordered, like OCaml's Map).
pub fn word_lengths(words: &[&str]) -> BTreeMap<String, usize> {
words
.iter()
.map(|w| (w.to_string(), w.len()))
.collect()
}
/// Filter entries by a predicate on values.
pub fn filter_by_value<K: Ord + Clone, V: Clone>(
map: &BTreeMap<K, V>,
pred: impl Fn(&V) -> bool,
) -> BTreeMap<K, V> {
map.iter()
.filter(|(_, v)| pred(v))
.map(|(k, v)| (k.clone(), v.clone()))
.collect()
}
/// Map over values, producing a new map.
pub fn map_values<K: Ord + Clone, V, U>(
map: &BTreeMap<K, V>,
f: impl Fn(&V) -> U,
) -> BTreeMap<K, U> {
map.iter()
.map(|(k, v)| (k.clone(), f(v)))
.collect()
}
/// Using HashMap for O(1) average lookup (unordered).
use std::collections::HashMap;
pub fn word_lengths_hash(words: &[&str]) -> HashMap<String, usize> {
words.iter().map(|w| (w.to_string(), w.len())).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_word_lengths() {
let words = vec!["ocaml", "rust", "haskell", "erlang", "go"];
let m = word_lengths(&words);
assert_eq!(m.get("rust"), Some(&4));
assert_eq!(m.get("haskell"), Some(&7));
assert_eq!(m.get("missing"), None);
}
#[test]
fn test_filter() {
let words = vec!["ocaml", "rust", "haskell", "erlang", "go"];
let m = word_lengths(&words);
let long = filter_by_value(&m, |v| *v > 4);
assert_eq!(long.len(), 3); // ocaml(5), haskell(7), erlang(6)
assert!(long.contains_key("haskell"));
assert!(!long.contains_key("go"));
}
#[test]
fn test_map_values() {
let words = vec!["rust", "go"];
let m = word_lengths(&words);
let doubled = map_values(&m, |v| v * 2);
assert_eq!(doubled.get("rust"), Some(&8));
assert_eq!(doubled.get("go"), Some(&4));
}
#[test]
fn test_empty() {
let m = word_lengths(&[]);
assert!(m.is_empty());
}
#[test]
fn test_hash_map() {
let words = vec!["rust", "go"];
let m = word_lengths_hash(&words);
assert_eq!(m.get("rust"), Some(&4));
}
#[test]
fn test_btree_ordering() {
let words = vec!["zebra", "apple", "mango"];
let m = word_lengths(&words);
let keys: Vec<_> = m.keys().collect();
assert_eq!(keys, vec!["apple", "mango", "zebra"]); // sorted
}
}
fn main() {
println!("{:?}", m.get("rust"), Some(&4));
println!("{:?}", m.get("haskell"), Some(&7));
println!("{:?}", m.get("missing"), None);
}
module StringMap = Map.Make(String)
let word_lengths words =
List.fold_left
(fun acc w -> StringMap.add w (String.length w) acc)
StringMap.empty
words
let () =
let words = ["ocaml"; "rust"; "haskell"; "erlang"; "go"] in
let m = word_lengths words in
assert (StringMap.find_opt "rust" m = Some 4);
assert (StringMap.find_opt "missing" m = None);
let long = StringMap.filter (fun _ v -> v > 4) m in
assert (StringMap.cardinal long = 3);
let doubled = StringMap.map (fun v -> v * 2) m in
assert (StringMap.find "rust" doubled = 8);
print_endline "All assertions passed."
📊 Detailed Comparison
Map.Make Functor — String→Int Dictionary: OCaml vs Rust
The Core Insight
Both languages provide immutable, ordered dictionary types backed by balanced trees. The fascinating difference is how they're parameterized: OCaml uses functors (functions from modules to modules) while Rust uses generics with trait bounds. Same goal, fundamentally different abstraction mechanisms.OCaml Approach
`Map.Make(String)` is a functor application — it takes the `String` module (which satisfies the `OrderedType` signature with a `compare` function) and produces a new module `StringMap` with all map operations specialized to string keys. The resulting map is an immutable balanced BST. Operations like `add`, `find_opt`, `filter`, and `map` all return new maps without mutating the original. This functor pattern is central to OCaml's module system.Rust Approach
Rust's `BTreeMap<K, V>` is a generic type parameterized by key and value types. The key must implement `Ord` (for ordering) — this is checked at compile time via trait bounds. Unlike OCaml's functor, you don't create a specialized module; you just use the generic type directly. Rust also offers `HashMap<K, V>` for O(1) average-case lookups (requires `Hash + Eq`). Iterator adapters (`filter`, `map`, `collect`) provide the functional transformation style.Side-by-Side
| Concept | OCaml | Rust |
|---|---|---|
| Ordered map | `Map.Make(String)` functor | `BTreeMap<String, V>` generic |
| Hash map | Third-party / Hashtbl (mutable) | `HashMap<K, V>` (in std) |
| Key constraint | `OrderedType` signature | `Ord` trait bound |
| Parameterization | Functor (module → module) | Generics (type → type) |
| Immutability | All operations return new map | Methods take `&self`, return new collections |
| Lookup | `find_opt : key -> 'a t -> 'a option` | `.get(&key) -> Option<&V>` |
What Rust Learners Should Notice
- `BTreeMap` is Rust's closest equivalent to OCaml's `Map` — both are balanced BSTs with O(log n) operations
- Rust's `.collect()` is incredibly powerful — it can build any collection from an iterator, guided by type inference
- OCaml functors create new modules at compile time; Rust generics monomorphize at compile time — both are zero-cost
- `HashMap` is usually preferred in Rust for performance unless you need ordered iteration
- Rust maps own their keys and values — `.get()` returns `Option<&V>` (a borrow), not a copy
Further Reading
- [The Rust Book — Hash Maps](https://doc.rust-lang.org/book/ch08-03-hash-maps.html)
- [Rust std::collections::BTreeMap](https://doc.rust-lang.org/std/collections/struct.BTreeMap.html)
- [OCaml Map module](https://ocaml.org/docs/maps)