Lean 4 (Meta)programming Cookbook

HashMap๐Ÿ”—

Contributors: Anirudh Gupta

HashMap is a collection of key-value pairs that provides efficient lookup, insertion, and deletion. In Lean 4, the most commonly used implementation is Std.HashMap.

Basic Operations๐Ÿ”—

To use HashMap, you usually need to provide types for keys and values. The key type must have Hashable and BEq instances.

def myMap : HashMap String Nat := {} -- Inserting values def updatedMap := myMap.insert "apple" 1 def updatedMap2 := updatedMap.insert "banana" 2 -- Accessing values (returns Option) some 1#eval updatedMap2.get? "apple" none#eval updatedMap2.get? "cherry" -- Checking for existence true#eval updatedMap2.contains "apple" -- Removing values def erasedMap := updatedMap2.erase "apple" false#eval erasedMap.contains "apple" -- Updating values using 'alter' /-- Increment the value for a key, or initialize it to 1 -/ def increment (map : HashMap String Nat) (key : String) : HashMap String Nat := map.alter key fun | some n => some (n + 1) | none => some 1 some 2#eval (increment updatedMap2 "apple").get? "apple"

Memoization with StateM๐Ÿ”—

Memoization is a technique used to speed up computer programs by storing the results of expensive function calls and returning the cached result when the same inputs occur again. A HashMap combined with a StateM monad is a powerful way to implement this in Lean. StateM allows us to carry the state of our cache (here HashMap) through our computations.

Below is an example of the Fibonacci sequence implemented with memoization.

/-- Recursive Fibonacci with memoization. We use StateM Memo to carry the cache. -/ abbrev FibState := HashMap Nat Nat abbrev FibM := StateM FibState def fib (n: Nat) : FibM Nat := do match n with | 0 => return 1 | 1 => return 1 | k + 2 => do let m โ† get match m.get? n with -- check if we calculated it before | some v => return v | none => do let v1 โ† fib k -- calculate at k and update the state let v2 โ† fib (k + 1) let v := v1 + v2 modify (fun m => m.insert n v) return v /- - `run` -> calculates given an initial state and returns the result and the final state - `run'` -> given an initial state returns the result -/ 10119911756749018713965376799211044556615579094364594923736162239653346274#eval fib 350 |>.run' {} (20365011074, Std.HashMap.ofList [(2, 2), (3, 3), (4, 5), (5, 8), (6, 13), (7, 21), (8, 34), (9, 55), (10, 89), (11, 144), (12, 233), (13, 377), (14, 610), (15, 987), (16, 1597), (17, 2584), (18, 4181), (19, 6765), (20, 10946), (21, 17711), (22, 28657), (23, 46368), (24, 75025), (25, 121393), (26, 196418), (27, 317811), (28, 514229), (29, 832040), (30, 1346269), (31, 2178309), (32, 3524578), (33, 5702887), (34, 9227465), (35, 14930352), (36, 24157817), (37, 39088169), (38, 63245986), (39, 102334155), (40, 165580141), (41, 267914296), (42, 433494437), (43, 701408733), (44, 1134903170), (45, 1836311903), (46, 2971215073), (47, 4807526976), (48, 7778742049), (49, 12586269025), (50, 20365011074)])#eval fib 50 |>.run {}

Using memoization allows us to compute fib 350 almost instantly, whereas a naive recursive implementation would take a very long time.

Application: Advanced Expression Analysis๐Ÿ”—

In metaprogramming, you might want to perform a multi-layered analysis of an expression. Below is an example that:

  1. Counts how many times each constant appears.

  2. Records the minimum depth at which each constant first appeared .

  3. Demonstrates merging two frequency maps.

  4. Shows how to filter a map to keep only specific entries.

structure ConstInfo where count : Nat firstDepth : Nat deriving Repr, Inhabited instance : ToString ConstInfo where toString info := s!"Count: {info.count}, First Depth: {info.firstDepth}" /-- An analyzer that tracks both count and the depth of the first occurrence. Using MetaM allows us to use 'logInfo' for debugging. -/ def analyzeExpr (e : Expr) (depth : Nat := 0) (emap : HashMap Name ConstInfo := {}) : MetaM (HashMap Name ConstInfo) := do match e with -- for constants | .const name _ => -- Simple debug print to the Infoview logInfo m!"Found constant: {name} at depth {depth}" return emap.alter name fun | some info => some { info with count := info.count + 1 } | none => some { count := 1, firstDepth := depth } -- for applications like `f a` | .app f a => let emap' โ† analyzeExpr f depth emap analyzeExpr a depth emap' -- for lambda and forall | .lam _ _ b _ | .forallE _ _ b _ => analyzeExpr b (depth + 1) emap | _ => return emap /-- We merge two HashMaps. If a constant exists in both, we sum the counts and take the minimum depth. -/ def mergeAnalysis (m1 m2 : HashMap Name ConstInfo) : HashMap Name ConstInfo := m2.fold (init := m1) fun acc name info2 => acc.alter name fun | some info1 => some { count := info1.count + info2.count, firstDepth := min info1.firstDepth info2.firstDepth } | none => some info2 def egExprCounter : MetaM Unit := do -- Using 'toExpr' elaborates numbers -- into 'OfNat.ofNat' calls let e1 โ† mkEq (toExpr 1) (toExpr 1) let e2 โ† mkEq (toExpr 2) (toExpr 3) let analysis1 โ† analyzeExpr e1 logInfo s!"Analysis of e1: {analysis1.toList.map (fun (k, v) => (k, s!"{v}"))}" let analysis2 โ† analyzeExpr e2 logInfo s!"Analysis of e2: {analysis2.toList.map (fun (k, v) => (k, s!"{v}"))}" -- Merge the results of two different expressions let combined := mergeAnalysis analysis1 analysis2 logInfo s!"Analysis of combined: {combined.toList.map (fun (k, v) => (k, s!"{v}"))}" -- filter the map to only include constants that -- appeared more than once let frequent := combined.toList.filter (ยท.2.count > 1) |> HashMap.ofList -- Map the values to just the counts for final display let finalCounts := frequent.toList.map fun (k, v) => (k, v.count) IO.println s!"Frequent constants: {finalCounts}" Frequent constants: [(Nat, 6), (instOfNatNat, 4), (Eq, 2), (OfNat.ofNat, 4)] Found constant: Eq at depth 0Found constant: Nat at depth 0Found constant: OfNat.ofNat at depth 0Found constant: Nat at depth 0Found constant: instOfNatNat at depth 0Found constant: OfNat.ofNat at depth 0Found constant: Nat at depth 0Found constant: instOfNatNat at depth 0Analysis of e1: [(Nat, Count: 3, First Depth: 0), (instOfNatNat, Count: 2, First Depth: 0), (Eq, Count: 1, First Depth: 0), (OfNat.ofNat, Count: 2, First Depth: 0)]Found constant: Eq at depth 0Found constant: Nat at depth 0Found constant: OfNat.ofNat at depth 0Found constant: Nat at depth 0Found constant: instOfNatNat at depth 0Found constant: OfNat.ofNat at depth 0Found constant: Nat at depth 0Found constant: instOfNatNat at depth 0Analysis of e2: [(Nat, Count: 3, First Depth: 0), (instOfNatNat, Count: 2, First Depth: 0), (Eq, Count: 1, First Depth: 0), (OfNat.ofNat, Count: 2, First Depth: 0)]Analysis of combined: [(Nat, Count: 6, First Depth: 0), (instOfNatNat, Count: 4, First Depth: 0), (Eq, Count: 2, First Depth: 0), (OfNat.ofNat, Count: 4, First Depth: 0)]#eval egExprCounter