ExamplesBy LevelBy TopicLearning Paths
960 Fundamental

960 Key Value Store

Functional Programming

Tutorial

The Problem

Implement a mutable key-value store backed by HashMap<String, String> with get/set/delete/contains/keys operations. The impl Into<String> pattern for set allows callers to pass &str or String without explicit conversion. Return sorted keys for deterministic output. Also implement an immutable functional variant using association lists.

🎯 Learning Outcomes

  • • Build a KvStore struct wrapping HashMap<String, String> with CRUD methods
  • • Use impl Into<String> for ergonomic insertion: accepts both &str and String at call sites
  • • Implement get returning Option<&str> (borrowed from the map's stored value)
  • • Implement sorted keys() by collecting, sorting, and returning Vec<&str>
  • • Contrast with an immutable association-list approach: Vec<(String, String)> updated via functional operations
  • Code Example

    #![allow(clippy::all)]
    // 960: Key-Value Store
    // OCaml: Hashtbl (mutable) or association list (functional)
    // Rust: HashMap<String, String> (mutable) or BTreeMap for sorted iteration
    
    use std::collections::HashMap;
    
    // Approach 1: HashMap-based mutable store
    pub struct KvStore {
        data: HashMap<String, String>,
    }
    
    impl KvStore {
        pub fn new() -> Self {
            KvStore {
                data: HashMap::new(),
            }
        }
    
        pub fn set(&mut self, key: impl Into<String>, value: impl Into<String>) {
            self.data.insert(key.into(), value.into());
        }
    
        pub fn get(&self, key: &str) -> Option<&str> {
            self.data.get(key).map(|s| s.as_str())
        }
    
        pub fn delete(&mut self, key: &str) -> bool {
            self.data.remove(key).is_some()
        }
    
        pub fn contains(&self, key: &str) -> bool {
            self.data.contains_key(key)
        }
    
        pub fn keys(&self) -> Vec<&str> {
            let mut keys: Vec<&str> = self.data.keys().map(|s| s.as_str()).collect();
            keys.sort();
            keys
        }
    
        pub fn values(&self) -> Vec<&str> {
            self.data.values().map(|s| s.as_str()).collect()
        }
    
        pub fn size(&self) -> usize {
            self.data.len()
        }
    
        pub fn is_empty(&self) -> bool {
            self.data.is_empty()
        }
    }
    
    impl Default for KvStore {
        fn default() -> Self {
            Self::new()
        }
    }
    
    // Approach 2: Functional (immutable) KV using Vec of pairs
    #[derive(Clone, Debug)]
    pub struct FunctionalStore<V: Clone> {
        data: Vec<(String, V)>,
    }
    
    impl<V: Clone> FunctionalStore<V> {
        pub fn new() -> Self {
            FunctionalStore { data: vec![] }
        }
    
        pub fn set(&self, key: impl Into<String>, value: V) -> Self {
            let key = key.into();
            let mut data: Vec<(String, V)> = self
                .data
                .iter()
                .filter(|(k, _)| k != &key)
                .cloned()
                .collect();
            data.insert(0, (key, value));
            FunctionalStore { data }
        }
    
        pub fn get(&self, key: &str) -> Option<&V> {
            self.data.iter().find(|(k, _)| k == key).map(|(_, v)| v)
        }
    
        pub fn delete(&self, key: &str) -> Self {
            let data = self
                .data
                .iter()
                .filter(|(k, _)| k != key)
                .cloned()
                .collect();
            FunctionalStore { data }
        }
    
        pub fn keys(&self) -> Vec<&str> {
            self.data.iter().map(|(k, _)| k.as_str()).collect()
        }
    }
    
    impl<V: Clone> Default for FunctionalStore<V> {
        fn default() -> Self {
            Self::new()
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_mutable_store() {
            let mut s = KvStore::new();
            assert_eq!(s.size(), 0);
            assert!(s.is_empty());
    
            s.set("name", "Alice");
            s.set("age", "30");
            s.set("city", "Amsterdam");
    
            assert_eq!(s.get("name"), Some("Alice"));
            assert_eq!(s.get("age"), Some("30"));
            assert_eq!(s.get("missing"), None);
            assert!(s.contains("city"));
            assert!(!s.contains("missing"));
            assert_eq!(s.size(), 3);
        }
    
        #[test]
        fn test_update() {
            let mut s = KvStore::new();
            s.set("name", "Alice");
            s.set("name", "Bob");
            assert_eq!(s.get("name"), Some("Bob"));
            assert_eq!(s.size(), 1);
        }
    
        #[test]
        fn test_delete() {
            let mut s = KvStore::new();
            s.set("name", "Alice");
            s.set("age", "30");
            let removed = s.delete("age");
            assert!(removed);
            assert_eq!(s.get("age"), None);
            assert_eq!(s.size(), 1);
            assert!(!s.delete("missing"));
        }
    
        #[test]
        fn test_keys_sorted() {
            let mut s = KvStore::new();
            s.set("city", "Amsterdam");
            s.set("name", "Alice");
            assert_eq!(s.keys(), vec!["city", "name"]);
        }
    
        #[test]
        fn test_functional_store() {
            let fs: FunctionalStore<i32> = FunctionalStore::new();
            let fs = fs.set("x", 1);
            let fs = fs.set("y", 2);
            let fs = fs.set("x", 10); // update
            assert_eq!(fs.get("x"), Some(&10));
            assert_eq!(fs.get("y"), Some(&2));
            assert_eq!(fs.get("z"), None);
    
            let fs2 = fs.delete("y");
            assert_eq!(fs2.get("y"), None);
            assert_eq!(fs.get("y"), Some(&2)); // original unchanged
        }
    }

    Key Differences

    AspectRustOCaml
    Hash tableHashMap — O(1) averageHashtbl — O(1) average (mutable)
    Ergonomic insertionimpl Into<String>Direct string — no conversion needed
    Functional variantVec<(String, String)> — O(n) lookupAssociation list — same
    Sorted keysManual .sort()List.sort String.compare
    Remove resultOption<String> — know if key existedunitHashtbl.remove is void

    HashMap does not guarantee key order. Use BTreeMap when sorted iteration matters for correctness (e.g., when serializing to a deterministic format). The keys() method here sorts on each call — for read-heavy usage, consider maintaining a sorted BTreeMap instead.

    OCaml Approach

    (* Mutable: Hashtbl *)
    module KvStore = struct
      type t = (string, string) Hashtbl.t
    
      let create () = Hashtbl.create 16
      let set tbl k v = Hashtbl.replace tbl k v
      let get tbl k = Hashtbl.find_opt tbl k
      let delete tbl k = Hashtbl.remove tbl k
      let contains tbl k = Hashtbl.mem tbl k
      let keys tbl =
        Hashtbl.fold (fun k _ acc -> k :: acc) tbl []
        |> List.sort String.compare
    end
    
    (* Functional: association list *)
    let set_al k v pairs = (k, v) :: List.filter (fun (k', _) -> k' <> k) pairs
    let get_al k pairs = List.assoc_opt k pairs
    let delete_al k pairs = List.filter (fun (k', _) -> k' <> k) pairs
    

    OCaml's Hashtbl is a mutable hash table similar to Rust's HashMap. The functional association list approach creates new lists on every update — persistent but O(n) for all operations.

    Full Source

    #![allow(clippy::all)]
    // 960: Key-Value Store
    // OCaml: Hashtbl (mutable) or association list (functional)
    // Rust: HashMap<String, String> (mutable) or BTreeMap for sorted iteration
    
    use std::collections::HashMap;
    
    // Approach 1: HashMap-based mutable store
    pub struct KvStore {
        data: HashMap<String, String>,
    }
    
    impl KvStore {
        pub fn new() -> Self {
            KvStore {
                data: HashMap::new(),
            }
        }
    
        pub fn set(&mut self, key: impl Into<String>, value: impl Into<String>) {
            self.data.insert(key.into(), value.into());
        }
    
        pub fn get(&self, key: &str) -> Option<&str> {
            self.data.get(key).map(|s| s.as_str())
        }
    
        pub fn delete(&mut self, key: &str) -> bool {
            self.data.remove(key).is_some()
        }
    
        pub fn contains(&self, key: &str) -> bool {
            self.data.contains_key(key)
        }
    
        pub fn keys(&self) -> Vec<&str> {
            let mut keys: Vec<&str> = self.data.keys().map(|s| s.as_str()).collect();
            keys.sort();
            keys
        }
    
        pub fn values(&self) -> Vec<&str> {
            self.data.values().map(|s| s.as_str()).collect()
        }
    
        pub fn size(&self) -> usize {
            self.data.len()
        }
    
        pub fn is_empty(&self) -> bool {
            self.data.is_empty()
        }
    }
    
    impl Default for KvStore {
        fn default() -> Self {
            Self::new()
        }
    }
    
    // Approach 2: Functional (immutable) KV using Vec of pairs
    #[derive(Clone, Debug)]
    pub struct FunctionalStore<V: Clone> {
        data: Vec<(String, V)>,
    }
    
    impl<V: Clone> FunctionalStore<V> {
        pub fn new() -> Self {
            FunctionalStore { data: vec![] }
        }
    
        pub fn set(&self, key: impl Into<String>, value: V) -> Self {
            let key = key.into();
            let mut data: Vec<(String, V)> = self
                .data
                .iter()
                .filter(|(k, _)| k != &key)
                .cloned()
                .collect();
            data.insert(0, (key, value));
            FunctionalStore { data }
        }
    
        pub fn get(&self, key: &str) -> Option<&V> {
            self.data.iter().find(|(k, _)| k == key).map(|(_, v)| v)
        }
    
        pub fn delete(&self, key: &str) -> Self {
            let data = self
                .data
                .iter()
                .filter(|(k, _)| k != key)
                .cloned()
                .collect();
            FunctionalStore { data }
        }
    
        pub fn keys(&self) -> Vec<&str> {
            self.data.iter().map(|(k, _)| k.as_str()).collect()
        }
    }
    
    impl<V: Clone> Default for FunctionalStore<V> {
        fn default() -> Self {
            Self::new()
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_mutable_store() {
            let mut s = KvStore::new();
            assert_eq!(s.size(), 0);
            assert!(s.is_empty());
    
            s.set("name", "Alice");
            s.set("age", "30");
            s.set("city", "Amsterdam");
    
            assert_eq!(s.get("name"), Some("Alice"));
            assert_eq!(s.get("age"), Some("30"));
            assert_eq!(s.get("missing"), None);
            assert!(s.contains("city"));
            assert!(!s.contains("missing"));
            assert_eq!(s.size(), 3);
        }
    
        #[test]
        fn test_update() {
            let mut s = KvStore::new();
            s.set("name", "Alice");
            s.set("name", "Bob");
            assert_eq!(s.get("name"), Some("Bob"));
            assert_eq!(s.size(), 1);
        }
    
        #[test]
        fn test_delete() {
            let mut s = KvStore::new();
            s.set("name", "Alice");
            s.set("age", "30");
            let removed = s.delete("age");
            assert!(removed);
            assert_eq!(s.get("age"), None);
            assert_eq!(s.size(), 1);
            assert!(!s.delete("missing"));
        }
    
        #[test]
        fn test_keys_sorted() {
            let mut s = KvStore::new();
            s.set("city", "Amsterdam");
            s.set("name", "Alice");
            assert_eq!(s.keys(), vec!["city", "name"]);
        }
    
        #[test]
        fn test_functional_store() {
            let fs: FunctionalStore<i32> = FunctionalStore::new();
            let fs = fs.set("x", 1);
            let fs = fs.set("y", 2);
            let fs = fs.set("x", 10); // update
            assert_eq!(fs.get("x"), Some(&10));
            assert_eq!(fs.get("y"), Some(&2));
            assert_eq!(fs.get("z"), None);
    
            let fs2 = fs.delete("y");
            assert_eq!(fs2.get("y"), None);
            assert_eq!(fs.get("y"), Some(&2)); // original unchanged
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_mutable_store() {
            let mut s = KvStore::new();
            assert_eq!(s.size(), 0);
            assert!(s.is_empty());
    
            s.set("name", "Alice");
            s.set("age", "30");
            s.set("city", "Amsterdam");
    
            assert_eq!(s.get("name"), Some("Alice"));
            assert_eq!(s.get("age"), Some("30"));
            assert_eq!(s.get("missing"), None);
            assert!(s.contains("city"));
            assert!(!s.contains("missing"));
            assert_eq!(s.size(), 3);
        }
    
        #[test]
        fn test_update() {
            let mut s = KvStore::new();
            s.set("name", "Alice");
            s.set("name", "Bob");
            assert_eq!(s.get("name"), Some("Bob"));
            assert_eq!(s.size(), 1);
        }
    
        #[test]
        fn test_delete() {
            let mut s = KvStore::new();
            s.set("name", "Alice");
            s.set("age", "30");
            let removed = s.delete("age");
            assert!(removed);
            assert_eq!(s.get("age"), None);
            assert_eq!(s.size(), 1);
            assert!(!s.delete("missing"));
        }
    
        #[test]
        fn test_keys_sorted() {
            let mut s = KvStore::new();
            s.set("city", "Amsterdam");
            s.set("name", "Alice");
            assert_eq!(s.keys(), vec!["city", "name"]);
        }
    
        #[test]
        fn test_functional_store() {
            let fs: FunctionalStore<i32> = FunctionalStore::new();
            let fs = fs.set("x", 1);
            let fs = fs.set("y", 2);
            let fs = fs.set("x", 10); // update
            assert_eq!(fs.get("x"), Some(&10));
            assert_eq!(fs.get("y"), Some(&2));
            assert_eq!(fs.get("z"), None);
    
            let fs2 = fs.delete("y");
            assert_eq!(fs2.get("y"), None);
            assert_eq!(fs.get("y"), Some(&2)); // original unchanged
        }
    }

    Deep Comparison

    Key-Value Store — Comparison

    Core Insight

    Both Hashtbl (OCaml) and HashMap (Rust) are hash-based mutable dictionaries. The API is nearly identical. The key difference: Rust wraps state in a struct with &mut self methods (enforcing ownership), while OCaml passes the table as a function argument. Both languages also support functional association lists for immutable KV storage.

    OCaml Approach

  • Hashtbl.create 16 creates a mutable table with initial capacity
  • Hashtbl.replace — set (handles both insert and update)
  • Hashtbl.find_opt — get returning option
  • Hashtbl.remove — delete key
  • Hashtbl.mem — contains check
  • Hashtbl.fold — iterate over all entries
  • • Functional alternative: (string * 'a) list with List.assoc_opt
  • Rust Approach

  • HashMap::new() wrapped in a struct for encapsulation
  • insert (returns old value), get, remove, contains_key
  • &self for reads, &mut self for writes — ownership enforced by compiler
  • impl Into<String> for flexible key/value input
  • • Functional alternative: Vec<(String, V)> with filter + find
  • Comparison Table

    AspectOCamlRust
    Hash tableHashtbl.tHashMap<K, V>
    CreateHashtbl.create 16HashMap::new()
    Insert/updateHashtbl.replace t k vmap.insert(k, v)
    LookupHashtbl.find_opt t kmap.get(k)Option<&V>
    DeleteHashtbl.remove t kmap.remove(k)
    ContainsHashtbl.mem t kmap.contains_key(k)
    IterationHashtbl.fold / Hashtbl.iter.iter() / .keys() / .values()
    EncapsulationModule functions + table argstruct with impl block
    MutationPassed by value (mutable)&mut self methods

    Exercises

  • Add a merge(other: &KvStore) method that copies all entries from other, with other winning on conflicts.
  • Implement update<F: Fn(&str) -> String>(key: &str, f: F) that applies a function to an existing value.
  • Add an event log: record each set and delete with a timestamp in a Vec<(Instant, Event)>.
  • Implement prefix_keys(prefix: &str) -> Vec<&str> that returns all keys starting with the given prefix.
  • Build a persistent_kv_store that serializes to/from a file using serde_json on HashMap<String, String>.
  • Open Source Repos