Collections & Arrays
Lean provides two main sequential collections: List (linked lists) andArray (dynamic arrays). Knowing when to use each is crucial.
Lists
List is a singly-linked list—the default collection in functional programming:
1-- Create lists with bracket syntax2def nums : List Nat := [1, 2, 3, 4, 5]3def empty : List String := []45-- Prepend with :: (cons)6def moreNums := 0 :: nums -- [0, 1, 2, 3, 4, 5]78-- Concatenate with ++9def combined := [1, 2] ++ [3, 4] -- [1, 2, 3, 4]1011-- Common operations12#eval nums.length -- 513#eval nums.head? -- some 114#eval nums.tail? -- some [2, 3, 4, 5]15#eval nums.reverse -- [5, 4, 3, 2, 1]16#eval nums.take 3 -- [1, 2, 3]17#eval nums.drop 2 -- [3, 4, 5]Arrays
Array is a dynamic array with O(1) random access:
1-- Create arrays with #[] syntax2def nums : Array Nat := #[1, 2, 3, 4, 5]3def empty : Array String := #[]45-- Random access with []6#eval nums[0]! -- 17#eval nums[2]! -- 389-- Safe access returns Option10#eval nums[10]? -- none1112-- Push elements (creates a new array)13def moreNums := nums.push 6 -- #[1, 2, 3, 4, 5, 6]1415-- Common operations16#eval nums.size -- 517#eval nums.toList -- [1, 2, 3, 4, 5]18#eval nums.reverse -- #[5, 4, 3, 2, 1]When to Use Which?
| Use Case | List | Array |
|---|---|---|
| Random access (index) | O(n) - slow | O(1) - fast |
| Prepend (add to front) | O(1) - fast | O(n) - slow |
| Append (add to end) | O(n) - slow | O(1)* - fast |
| Pattern matching | Natural | Less common |
| Proofs & reasoning | Preferred | More complex |
Complexity Tips
A quick mental model: Lists are great for building up results and pattern matching, while Arrays are great for indexing and bulk computation. When performance matters, convert to the structure that matches the dominant operation.
List for pattern matching, proofs, and when building from the front. Use Array for computation, random access, and performance-critical code.Transforming Collections
Both List and Array support similar transformation methods:
1def nums := [1, 2, 3, 4, 5]23-- Map: apply a function to each element4#eval nums.map (· * 2) -- [2, 4, 6, 8, 10]5#eval nums.map toString -- ["1", "2", "3", "4", "5"]67-- Filter: keep elements matching a predicate8#eval nums.filter (· > 2) -- [3, 4, 5]9#eval nums.filter (· % 2 == 0) -- [2, 4]1011-- FilterMap: map and filter in one step12#eval nums.filterMap (fun n => if n > 2 then some (n * 10) else none)13-- [30, 40, 50]1415-- Find: first element matching a predicate16#eval nums.find? (· > 3) -- some 4Folding and Reducing
Fold operations reduce a collection to a single value:
1def nums := [1, 2, 3, 4, 5]23-- foldl: fold from left to right4#eval nums.foldl (· + ·) 0 -- 15 (sum)5#eval nums.foldl (· * ·) 1 -- 120 (product)6#eval nums.foldl max 0 -- 5 (maximum)78-- Build a string9#eval nums.foldl (fun acc n => acc ++ toString n) "" -- "12345"1011-- foldr: fold from right to left12#eval nums.foldr (· :: ·) [] -- [1, 2, 3, 4, 5] (copy)1314-- Specialized operations15#eval nums.sum -- 15 (requires Add instance)16#eval nums.all (· > 0) -- true17#eval nums.any (· > 4) -- trueRanges
Create ranges of numbers with [start:stop] syntax:
1-- Ranges (exclusive end)2#eval [0:5] -- [0, 1, 2, 3, 4]3#eval [1:10:2] -- [1, 3, 5, 7, 9] (step of 2)45-- Convert range to List6#eval (List.range 5) -- [0, 1, 2, 3, 4]78-- Useful for generating test data9def squares := (List.range 10).map (fun n => n * n)10#eval squares -- [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]Deep Dive: Array Performance
Arrays in Lean use a clever optimization: when an array has only one reference, mutations happen in-place. This means code that looks functional can be as efficient as imperative code.
1-- This is efficient because 'arr' is only referenced once2def buildArray (n : Nat) : Array Nat := Id.run do3 let mut arr := #[]4 for i in [0:n] do5 arr := arr.push i6 return arrThe compiler detects single-ownership and optimizes accordingly.
Zipping Collections
Combine two collections element-wise:
1def names := ["Alice", "Bob", "Carol"]2def ages := [30, 25, 35]34-- Zip into pairs5#eval names.zip ages6-- [("Alice", 30), ("Bob", 25), ("Carol", 35)]78-- Zip with a function9#eval names.zipWith ages (fun name age => s!"{name}: {age}")10-- ["Alice: 30", "Bob: 25", "Carol: 35"]1112-- Enumerate: add indices13#eval names.enum14-- [(0, "Alice"), (1, "Bob"), (2, "Carol")]Converting Between List and Array
1def myList : List Nat := [1, 2, 3]2def myArray : Array Nat := #[1, 2, 3]34-- List to Array5#eval myList.toArray -- #[1, 2, 3]67-- Array to List 8#eval myArray.toList -- [1, 2, 3]910-- Chain operations11#eval #[1, 2, 3].toList.reverse.toArray -- #[3, 2, 1]HashMap and HashSet
For efficient key-value storage and membership testing, use Std.HashMap andStd.HashSet:
1import Std.Data.HashMap2import Std.Data.HashSet34open Std56-- Create a HashMap7def ages : HashMap String Nat := HashMap.empty8 |>.insert "Alice" 309 |>.insert "Bob" 251011-- Lookup12#eval ages.find? "Alice" -- some 3013#eval ages.find? "Unknown" -- none1415-- Check membership16#eval ages.contains "Bob" -- true1718-- HashSet for unique values19def seen : HashSet String := HashSet.empty20 |>.insert "a"21 |>.insert "b" 22 |>.insert "a" -- Duplicate, ignored2324#eval seen.size -- 225#eval seen.contains "a" -- trueBEq and Hashable instances for the key type. Most standard types already have these.Strings as Collections
Strings can be treated as collections of characters:
1-- Iterate over characters2#eval "hello".toList -- ['h', 'e', 'l', 'l', 'o']3#eval "hello".data -- Same thing45-- Map over characters 6#eval "hello".map Char.toUpper -- "HELLO"78-- Filter characters9#eval "he11o".filter Char.isAlpha -- "heo"1011-- Fold characters12#eval "hello".foldl (fun acc c => acc ++ toString c ++ "-") ""13-- "h-e-l-l-o-"1415-- Check all/any16#eval "HELLO".all Char.isUpper -- true17#eval "Hello".any Char.isLower -- trueConvert an Array to a List, filter even numbers, and convert back to an array.
1def nums : Array Nat := #[1, 2, 3, 4, 5, 6]23def evens : Array Nat :=4 nums.toList.filter (· % 2 == 0) |>.toArray56#eval evens -- #[2, 4, 6]