This document describes the immutable collection data structures available in GALA's collection_immutable package.
- Overview
- Performance Characteristics
- List[T]
- Array[T]
- HashSet[T]
- TreeSet[T]
- HashMap[K,V]
- TreeMap[K,V]
- Choosing the Right Collection
- Implementation Details
- Performance Benchmarks
- Examples
The collection_immutable package provides persistent (immutable) data structures with performance characteristics matching Scala's immutable collections.
import . "martianoff/gala/collection_immutable"
| Operation | List | Array |
|---|---|---|
| Head | O(1) | O(eC) |
| Tail | O(1) | O(n) |
| Prepend | O(1) | O(1)* |
| Append | O(n) | O(eC) |
| Lookup | O(n) | O(eC) |
| Update | O(n) | O(eC) |
| Contains | O(n) | O(n) |
| Length | O(1) | O(1) |
| Operation | HashSet | TreeSet |
|---|---|---|
| Add | O(eC) | O(log n) |
| Remove | O(eC) | O(log n) |
| Contains | O(eC) | O(log n) |
| Min/Max | O(n) | O(log n) |
| Range | O(n) | O(log n + k) |
| Union | O(m) | O(m log n) |
| Intersect | O(m) | O(m log n) |
| Diff | O(n) | O(n log m) |
| Size | O(1) | O(1) |
| Operation | HashMap | TreeMap |
|---|---|---|
| Put | O(eC) | O(log n) |
| Get | O(eC) | O(log n) |
| Remove | O(eC) | O(log n) |
| Contains | O(eC) | O(log n) |
| Min/Max Key | O(n) | O(log n) |
| Range | O(n) | O(log n + k) |
| Size | O(1) | O(1) |
Legend:
- O(1) - Constant time
- O(1)* - Amortized constant time (uses prefix buffer, consolidates every 32 prepends)
- O(n) - Linear time (n = this collection's size)
- O(m) - Linear in smaller set (m = min(this.size, other.size))
- O(eC) - Effectively constant (O(log32 n) ≈ 7 operations for 1 billion elements)
An immutable singly-linked list. Best for stack-like operations (prepend, head, tail).
// Empty list
val empty = EmptyList[int]()
// From elements
val list = ListOf(1, 2, 3, 4, 5)
// Using Prepend (creates new list with element at front)
val list2 = EmptyList[int]().Prepend(3).Prepend(2).Prepend(1) // List(1, 2, 3)
val list = ListOf(1, 2, 3, 4, 5)
list.IsEmpty() // false
list.NonEmpty() // true
list.Length() // 5
list.Size() // 5 (alias for Length)
val list = ListOf(1, 2, 3)
// Head - first element
list.Head() // 1
list.HeadOption() // Some(1)
// Tail - all except first
list.Tail() // List(2, 3)
list.TailOption() // Some(List(2, 3))
// Last - last element
list.Last() // 3
list.LastOption() // Some(3)
// Init - all except last
list.Init() // List(1, 2)
val list = ListOf(10, 20, 30)
list.Get(0) // 10
list.Get(1) // 20
list.GetOption(1) // Some(20)
list.GetOption(10) // None
// Update at index (returns new list)
list.Updated(1, 99) // List(10, 99, 30)
val list = ListOf(2, 3, 4)
// Prepend - O(1)
list.Prepend(1) // List(1, 2, 3, 4)
// PrependAll
list.PrependAll(ListOf(0, 1)) // List(0, 1, 2, 3, 4)
// Append - O(n)
list.Append(5) // List(2, 3, 4, 5)
// AppendAll
list.AppendAll(ListOf(5, 6)) // List(2, 3, 4, 5, 6)
val list = ListOf(1, 2, 3, 4, 5)
list.Take(3) // List(1, 2, 3)
list.Drop(2) // List(3, 4, 5)
list.TakeWhile((x) => x < 4) // List(1, 2, 3)
list.DropWhile((x) => x < 3) // List(3, 4, 5)
list.SplitAt(2) // Tuple(List(1, 2), List(3, 4, 5))
val list = ListOf(1, 2, 3, 4, 5)
list.Contains(3) // true
list.IndexOf(3) // 2
list.IndexOf(10) // -1
list.Find((x) => x > 3) // Some(4)
val list = ListOf(1, 2, 3)
// Map
list.Map((x) => x * 2) // List(2, 4, 6)
// FlatMap
list.FlatMap((x) => ListOf(x, x * 10))
// List(1, 10, 2, 20, 3, 30)
// Filter
list.Filter((x) => x % 2 == 1) // List(1, 3)
list.FilterNot((x) => x % 2 == 1) // List(2)
// Collect - combines Filter and Map in a single pass using partial functions
list.Collect({ case x if x % 2 == 1 => x * 10 })
// List(10, 30)
// Partition
list.Partition((x) => x > 2)
// Tuple(List(3), List(1, 2))
// Reverse
list.Reverse() // List(3, 2, 1)
// Distinct
ListOf(1, 2, 2, 3, 1).Distinct() // List(1, 2, 3)
val list = ListOf(1, 2, 3, 4)
// FoldLeft
list.FoldLeft(0, (acc, x) => acc + x) // 10
// FoldRight
list.FoldRight(0, (x, acc) => x + acc) // 10
// Reduce
list.Reduce((a, b) => a + b) // 10
// ReduceOption (safe for empty lists)
list.ReduceOption((a, b) => a + b) // Some(10)
val list = ListOf(2, 4, 6, 8)
list.Exists((x) => x == 4) // true
list.ForAll((x) => x % 2 == 0) // true
list.Count((x) => x > 4) // 2
val nums = ListOf(1, 2, 3)
val strs = ListOf("a", "b", "c")
nums.Zip(strs)
// List(Tuple(1, "a"), Tuple(2, "b"), Tuple(3, "c"))
nums.ZipWithIndex()
// List(Tuple(1, 0), Tuple(2, 1), Tuple(3, 2))
val list = ListOf("banana", "apple", "cherry")
list.Sorted() // List("apple", "banana", "cherry")
list.SortWith((a, b) => a > b) // List("cherry", "banana", "apple")
list.SortBy((s) => len(s)) // List("apple", "banana", "cherry")
val list = ListOf(1, 2, 3)
list.ToSlice() // []int{1, 2, 3}
list.ToArray() // Array(1, 2, 3)
list.String() // "List(1, 2, 3)"
list.MkString(", ") // "1, 2, 3"
val nested = ListOf(
ListOf(1, 2),
ListOf(3, 4),
)
Flatten[int](nested) // List(1, 2, 3, 4)
val list = ListOf(1, 2, 3)
val result = list match {
case Cons(head, tail) => head // Matches non-empty list
case Nil() => -1 // Matches empty list
case _ => -2
}
list.ForEach((x) => {
Println(x)
})
An immutable indexed sequence with effectively constant time for most operations. Best for random access and append operations.
// Empty array
val empty Array[int] = EmptyArray[int]()
// From elements
val arr = ArrayOf(1, 2, 3, 4, 5)
// From slice
val slice = SliceOf(1, 2, 3)
val arr2 = ArrayFrom(slice)
// Tabulate: compute each element from index (O(n) via arrayBuilder)
val squares = ArrayTabulate(5, (i) => i * i)
// squares == Array(0, 1, 4, 9, 16)
// Fill: repeat the same value (O(n) via arrayBuilder)
val zeros = ArrayFill(3, 0)
// zeros == Array(0, 0, 0)
val arr = ArrayOf(1, 2, 3, 4, 5)
arr.IsEmpty() // false
arr.NonEmpty() // true
arr.Length() // 5
arr.Size() // 5
val arr = ArrayOf(1, 2, 3)
arr.Head() // 1
arr.HeadOption() // Some(1)
arr.Last() // 3
arr.LastOption() // Some(3)
arr.Tail() // Array(2, 3)
arr.TailOption() // Some(Array(2, 3))
arr.Init() // Array(1, 2)
val arr = ArrayOf(10, 20, 30)
arr.Get(0) // 10
arr.Get(1) // 20
arr.GetOption(1) // Some(20)
arr.GetOption(10) // None
// Update at index (returns new array) - O(eC)
arr.Updated(1, 99) // Array(10, 99, 30)
val arr = ArrayOf(2, 3, 4)
// Append - O(eC)
arr.Append(5) // Array(2, 3, 4, 5)
// AppendAll
arr.AppendAll(ArrayOf(5, 6)) // Array(2, 3, 4, 5, 6)
// Prepend - O(1) amortized (uses prefix buffer)
arr.Prepend(1) // Array(1, 2, 3, 4)
// PrependAll
arr.PrependAll(ArrayOf(0, 1)) // Array(0, 1, 2, 3, 4)
val arr = ArrayOf(1, 2, 3, 4, 5)
arr.Take(3) // Array(1, 2, 3)
arr.Drop(2) // Array(3, 4, 5)
arr.Slice(1, 4) // Array(2, 3, 4)
arr.TakeWhile((x) => x < 4) // Array(1, 2, 3)
arr.DropWhile((x) => x < 3) // Array(3, 4, 5)
arr.SplitAt(2) // Tuple(Array(1, 2), Array(3, 4, 5))
val arr = ArrayOf(1, 2, 3, 2, 1)
arr.Contains(3) // true
arr.IndexOf(2) // 1
arr.LastIndexOf(2) // 3
arr.Find((x) => x > 2) // Some(3)
arr.FindLast((x) => x < 3) // Some(1)
val arr = ArrayOf(1, 2, 3)
// Map
arr.Map((x) => x * 2) // Array(2, 4, 6)
// FlatMap
arr.FlatMap((x) => ArrayOf(x, x * 10))
// Array(1, 10, 2, 20, 3, 30)
// Filter
arr.Filter((x) => x % 2 == 1) // Array(1, 3)
arr.FilterNot((x) => x % 2 == 1) // Array(2)
// Collect - combines Filter and Map in a single pass using partial functions
arr.Collect({ case x if x % 2 == 0 => x * 10 })
// Array(20)
// Partition
arr.Partition((x) => x > 2)
// Tuple(Array(3), Array(1, 2))
// Reverse
arr.Reverse() // Array(3, 2, 1)
// Distinct
ArrayOf(1, 2, 2, 3, 1).Distinct() // Array(1, 2, 3)
val arr = ArrayOf(1, 2, 3, 4)
arr.FoldLeft(0, (acc, x) => acc + x) // 10
arr.FoldRight(0, (x, acc) => x + acc) // 10
arr.Reduce((a, b) => a + b) // 10
arr.ReduceOption((a, b) => a + b) // Some(10)
val arr = ArrayOf(2, 4, 6, 8)
arr.Exists((x) => x == 4) // true
arr.ForAll((x) => x % 2 == 0) // true
arr.Count((x) => x > 4) // 2
val nums = ArrayOf(1, 2, 3)
val strs = ArrayOf("a", "b", "c")
nums.Zip(strs)
// Array(Tuple(1, "a"), Tuple(2, "b"), Tuple(3, "c"))
nums.ZipWithIndex()
// Array(Tuple(1, 0), Tuple(2, 1), Tuple(3, 2))
val arr = ArrayOf(1, 2, 3, 4, 5)
// Split into groups of size n
arr.Grouped(2)
// Array(Array(1, 2), Array(3, 4), Array(5))
// Sliding window
arr.Sliding(3)
// Array(Array(1, 2, 3), Array(2, 3, 4), Array(3, 4, 5))
val arr = ArrayOf(3, 1, 4, 1, 5, 9)
// Natural ordering
arr.Sorted() // Array(1, 1, 3, 4, 5, 9)
// Custom comparator (descending)
arr.SortWith((a, b) => a > b) // Array(9, 5, 4, 3, 1, 1)
// Sort by key function
val arr2 = ArrayOf(1, 9, 3, 7, 5)
arr2.SortBy((x) => x) // Array(1, 3, 5, 7, 9)
val arr = ArrayOf(1, 2, 3)
arr.ToSlice() // []int{1, 2, 3}
arr.ToList() // List(1, 2, 3)
arr.String() // "Array(1, 2, 3)"
arr.MkString(", ") // "1, 2, 3"
arr.ForEach((x) => {
Println(x)
})
An immutable set with effectively constant time operations. Uses a Hash Array Mapped Trie (HAMT) structure similar to Scala's HashSet.
Type Requirements: T must be comparable and either:
- A primitive type (int, string, bool, float, etc.) - hashed automatically
- A custom type implementing the
Hashableinterface
Custom types must implement the Hashable interface to be used in HashSet:
// The Hashable interface
type Hashable interface {
Hash() uint32
}
// Example: Custom type implementing Hashable
type Person struct {
Name string
Age int
}
func (p Person) Hash() uint32 {
return HashCombine(HashString(p.Name), HashInt(int64(p.Age)))
}
// Now Person can be used in HashSet
val people = HashSetOf(
Person(Name = "Alice", Age = 30),
Person(Name = "Bob", Age = 25),
)
Available hash helper functions:
HashInt(n int64) uint32- Hash integersHashUint(n uint64) uint32- Hash unsigned integersHashString(s string) uint32- Hash stringsHashBool(b bool) uint32- Hash booleansHashCombine(h1, h2 uint32) uint32- Combine two hashes (for structs)
// Empty set
val empty = EmptyHashSet[int]()
// From elements
val set = HashSetOf(1, 2, 3, 4, 5)
// From slice
val slice = SliceOf(1, 2, 3)
val set2 = HashSetFromSlice(slice)
val set = HashSetOf(1, 2, 3, 4, 5)
set.IsEmpty() // false
set.NonEmpty() // true
set.Size() // 5
set.Length() // 5 (alias for Size)
val set = HashSetOf(1, 2, 3)
// Add element (returns new set)
set.Add(4) // HashSet(1, 2, 3, 4)
// Remove element (returns new set)
set.Remove(2) // HashSet(1, 3)
// Check membership
set.Contains(2) // true
set.Contains(10) // false
val a = HashSetOf(1, 2, 3, 4)
val b = HashSetOf(3, 4, 5, 6)
// Union - all elements from both sets
a.Union(b) // HashSet(1, 2, 3, 4, 5, 6)
// Intersection - elements in both sets
a.Intersect(b) // HashSet(3, 4)
// Difference - elements in a but not in b
a.Diff(b) // HashSet(1, 2)
// Subset check
HashSetOf(1, 2).SubsetOf(a) // true
val set = HashSetOf(1, 2, 3, 4, 5)
// Filter
set.Filter((x) => x % 2 == 0) // HashSet(2, 4)
set.FilterNot((x) => x % 2 == 0) // HashSet(1, 3, 5)
// Collect - combines Filter and Map in a single pass using partial functions
set.Collect({ case x if x % 2 == 0 => x * 10 })
// HashSet(20, 40)
// Partition
set.Partition((x) => x > 3)
// Tuple(HashSet(4, 5), HashSet(1, 2, 3))
// Map (use standalone function)
MapHashSet[int, int](set, (x) => x * 2) // HashSet(2, 4, 6, 8, 10)
val set = HashSetOf(1, 2, 3, 4, 5)
// FoldLeft
set.FoldLeft(0, (acc, x) => acc + x) // 15
// Reduce
set.Reduce((a, b) => a + b) // 15
// ReduceOption (safe for empty sets)
set.ReduceOption((a, b) => a + b) // Some(15)
val set = HashSetOf(2, 4, 6, 8)
set.Exists((x) => x > 5) // true
set.ForAll((x) => x % 2 == 0) // true
set.Count((x) => x > 4) // 2
set.Find((x) => x > 5) // Some(6) or Some(8)
Returns Array[T] since sets have no inherent order.
val set = HashSetOf(5, 3, 1)
set.Sorted() // Array(1, 3, 5)
set.SortWith((a, b) => a > b) // Array(5, 3, 1)
set.SortBy((x) => x) // Array(1, 3, 5)
val set = HashSetOf(1, 2, 3)
set.ToSlice() // []int{1, 2, 3} (order not guaranteed)
set.ToList() // List(1, 2, 3) (order not guaranteed)
set.ToArray() // Array(1, 2, 3) (order not guaranteed)
set.String() // "HashSet(1, 2, 3)"
set.MkString(", ") // "1, 2, 3" (order not guaranteed)
set.ForEach((x) => {
Println(x)
})
val set = HashSetOf(1, 2, 3)
val result = set match {
case s: HashSet[_] if s.IsEmpty() => "empty"
case s: HashSet[_] => s"has ${s.Size()} elements"
case _ => "unknown"
}
An immutable sorted set implemented as a Red-Black tree. Maintains elements in sorted order and provides O(log n) operations with additional features like min/max and range queries.
Type Requirements: T must be comparable and either:
- A primitive type (int, string, float64, etc.) - compared automatically
- A custom type implementing the
Ordered[T]interface
Custom types must implement the Ordered[T] interface to be used in TreeSet:
// The Ordered interface
type Ordered[T any] interface {
Compare(other T) int // Returns -1, 0, or 1
}
// Example: Custom type implementing Ordered
type Person struct {
Name string
Age int
}
func (p Person) Compare(other Person) int {
if p.Age < other.Age { return -1 }
if p.Age > other.Age { return 1 }
return 0
}
// Now Person can be used in TreeSet (sorted by age)
val people = TreeSetOf(
Person(Name = "Alice", Age = 30),
Person(Name = "Bob", Age = 25),
)
// people.Min() returns Person("Bob", 25)
// Empty set
val empty = EmptyTreeSet[int]()
// From elements
val set = TreeSetOf(1, 2, 3, 4, 5)
// From slice
val slice = SliceOf(3, 1, 4, 1, 5)
val set2 = TreeSetFromSlice(slice) // TreeSet(1, 3, 4, 5) - sorted, no duplicates
val set = TreeSetOf(5, 3, 1, 4, 2)
set.IsEmpty() // false
set.NonEmpty() // true
set.Size() // 5
set.Length() // 5 (alias for Size)
val set = TreeSetOf(1, 2, 3)
// Add element (returns new set)
set.Add(4) // TreeSet(1, 2, 3, 4)
// Remove element (returns new set)
set.Remove(2) // TreeSet(1, 3)
// Check membership
set.Contains(2) // true
set.Contains(10) // false
TreeSet's main advantage over HashSet: efficient min/max access.
val set = TreeSetOf(5, 3, 1, 4, 2)
// Min - smallest element
set.Min() // 1
set.MinOption() // Some(1)
// Max - largest element
set.Max() // 5
set.MaxOption() // Some(5)
// Head/Last (aliases for Min/Max)
set.Head() // 1 (same as Min)
set.Last() // 5 (same as Max)
val set = TreeSetOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
// Range [from, to] inclusive
set.Range(3, 7) // TreeSet(3, 4, 5, 6, 7)
// Range from (>= value)
set.RangeFrom(7) // TreeSet(7, 8, 9, 10)
// Range to (<= value)
set.RangeTo(4) // TreeSet(1, 2, 3, 4)
val a = TreeSetOf(1, 2, 3, 4)
val b = TreeSetOf(3, 4, 5, 6)
// Union - all elements from both sets
a.Union(b) // TreeSet(1, 2, 3, 4, 5, 6)
// Intersection - elements in both sets
a.Intersect(b) // TreeSet(3, 4)
// Difference - elements in a but not in b
a.Diff(b) // TreeSet(1, 2)
// Subset check
TreeSetOf(1, 2).SubsetOf(a) // true
val set = TreeSetOf(1, 2, 3, 4, 5)
// Filter
set.Filter((x) => x % 2 == 0) // TreeSet(2, 4)
set.FilterNot((x) => x % 2 == 0) // TreeSet(1, 3, 5)
// Partition
set.Partition((x) => x > 3)
// Tuple(TreeSet(4, 5), TreeSet(1, 2, 3))
// Map (use standalone function)
MapTreeSet(set, (x) => x * 2) // TreeSet(2, 4, 6, 8, 10)
val set = TreeSetOf(1, 2, 3, 4, 5)
// FoldLeft - processes elements in sorted order!
set.FoldLeft(0, (acc, x) => acc + x) // 15
// Reduce
set.Reduce((a, b) => a + b) // 15
// ReduceOption (safe for empty sets)
set.ReduceOption((a, b) => a + b) // Some(15)
val set = TreeSetOf(2, 4, 6, 8)
set.Exists((x) => x > 5) // true
set.ForAll((x) => x % 2 == 0) // true
set.Count((x) => x > 4) // 2
set.Find((x) => x > 5) // Some(6) - first in sorted order
Returns Array[T]. Sorted() returns elements in natural order (already sorted by tree structure).
val set = TreeSetOf(30, 10, 20)
set.Sorted() // Array(10, 20, 30)
set.SortWith((a, b) => a > b) // Array(30, 20, 10)
set.SortBy((x) => x) // Array(10, 20, 30)
val set = TreeSetOf(3, 1, 2)
set.ToSlice() // []int{1, 2, 3} - sorted order
set.ToList() // List(1, 2, 3) - sorted order
set.ToArray() // Array(1, 2, 3) - sorted order
set.ToHashSet() // HashSet(1, 2, 3) - loses order, gains O(eC) lookup
set.String() // "TreeSet(1, 2, 3)"
set.MkString(", ") // "1, 2, 3" - sorted order
// Elements processed in sorted order
set.ForEach((x) => {
Println(x) // Prints: 1, 2, 3 (in order)
})
val set = TreeSetOf(1, 2, 3)
val result = set match {
case s: TreeSet[_] if s.IsEmpty() => "empty"
case s: TreeSet[_] => s"has ${s.Size()} elements, min=${s.Min()}"
case _ => "unknown"
}
An immutable hash-based map with effectively constant time operations. See collection_immutable/hashmap.gala for the full API.
val empty = EmptyHashMap[string, int]()
val m = HashMapOf(("a", 1), ("b", 2), ("c", 3))
val m = HashMapOf(("a", 1), ("b", 2))
m.Put("c", 3) // new map with added entry
m.Get("a") // Some(1)
m.GetOrElse("z", 0) // 0
m.Contains("a") // true
m.Remove("b") // new map without "b"
m.Size() // 2
val m = HashMapOf(("a", 1), ("b", 2), ("c", 3))
m.Keys() // HashSet("a", "b", "c")
m.Keys().Sorted() // Array("a", "b", "c") — sorted keys
m.Keys().ToArray() // Array (unordered)
Note:
HashMap.Sorted()requires bothKandVto be orderable types. IfVis not orderable (e.g., a struct or sealed type), usem.Keys().Sorted()instead to get sorted keys, then look up values individually.
val m = HashMapOf(("a", 1), ("b", 2), ("c", 3))
m.Filter((k, v) => v > 1) // HashMap(b -> 2, c -> 3)
m.MapValues((v) => v * 2) // HashMap(a -> 2, b -> 4, c -> 6)
m.FoldLeftKV(0, (acc, k, v) => acc + v) // 6
m.FoldLeft(0, (acc, entry) => acc + entry.V2) // 6 — iterates as Tuple entries
m.Sorted() // Array((a,1), (b,2), (c,3))
// Collect - applies a partial function to each entry, returns Array[U]
m.Collect({ case (k, v) if v > 1 => k })
// Array("b", "c")
// Filter by keys only
m.FilterKeys((k) => k > "b")
// Filter by values only
m.FilterValues((v) => v > 1)
m.ForEachKV((k, v) => {
Println(s"$k -> $v")
})
// Iterate keys only
m.ForEachKey((k) => Println(k))
// Iterate values only
m.ForEachValue((v) => Println(v))
An immutable sorted map implemented as a Red-Black tree. Maintains entries in sorted key order and provides O(log n) operations with range queries.
Type Requirements: Keys must be comparable and either:
- A primitive type (int, string, float64, etc.) - compared automatically
- A custom type implementing the
Ordered[T]interface
// Empty map
val empty = EmptyTreeMap[string, int]()
// From entries
val m = TreeMapOf(("cherry", 3), ("apple", 1), ("banana", 2))
// TreeMap(apple -> 1, banana -> 2, cherry -> 3) -- sorted by key
// From slice of tuples
val m2 = TreeMapFromSlice(SliceOf(("a", 1), ("b", 2)))
// From Go map
var goMap = map[string]int{"x": 10, "y": 20}
val m3 = TreeMapFromGoMap(goMap)
val m = TreeMapOf(("a", 1), ("b", 2), ("c", 3))
// Put (returns new map)
val m2 = m.Put("d", 4)
// Get
m.Get("b") // Some(2)
m.Get("z") // None
m.GetOrElse("z", 0) // 0
m.Apply("a") // 1 (panics if missing)
// Contains
m.Contains("a") // true
// Remove (returns new map)
val m3 = m.Remove("b") // TreeMap(a -> 1, c -> 3)
// Size
m.Size() // 3
val m = TreeMapOf(("a", 1), ("b", 2), ("c", 3))
m.MinKey() // "a"
m.MaxKey() // "c"
m.MinEntry() // Tuple("a", 1)
m.MaxEntry() // Tuple("c", 3)
m.MinKeyOption() // Some("a")
m.MaxKeyOption() // Some("c")
val m = TreeMapOf(("a", 1), ("b", 2), ("c", 3), ("d", 4), ("e", 5))
// Range [from, to] inclusive
m.Range("b", "d") // TreeMap(b -> 2, c -> 3, d -> 4)
// RangeFrom (>= key)
m.RangeFrom("c") // TreeMap(c -> 3, d -> 4, e -> 5)
// RangeTo (<= key)
m.RangeTo("c") // TreeMap(a -> 1, b -> 2, c -> 3)
val m = TreeMapOf(("a", 1), ("b", 2), ("c", 3), ("d", 4))
// Filter
m.Filter((k, v) => v % 2 == 0) // TreeMap(b -> 2, d -> 4)
m.FilterKeys((k) => k > "b") // TreeMap(c -> 3, d -> 4)
m.FilterValues((v) => v > 2) // TreeMap(c -> 3, d -> 4)
// MapValues
m.MapValues((v) => v * 10) // TreeMap(a -> 10, b -> 20, c -> 30, d -> 40)
// FoldLeftKV
m.FoldLeftKV(0, (acc, k, v) => acc + v) // 10
// Merge with combiner
val m2 = TreeMapOf(("c", 30), ("e", 5))
m.Merge(m2, (v1, v2) => v1 + v2)
// TreeMap(a -> 1, b -> 2, c -> 33, d -> 4, e -> 5)
// Iteration (sorted key order)
m.ForEachKV((k, v) => { Println(s"$k=$v") })
// a=1 b=2 c=3 d=4
val m = TreeMapOf(("c", 30), ("a", 10), ("b", 20))
// Sorted by key (no-op, already sorted)
m.Sorted() // Array((a,10), (b,20), (c,30))
// Custom ordering (sort by value)
m.SortWith((a, b) => a.V2 < b.V2) // Array((a,10), (b,20), (c,30))
// Sort by extracted key (descending by value)
m.SortBy((e) => -e.V2) // Array((c,30), (b,20), (a,10))
val m = TreeMapOf(("a", 1), ("b", 2), ("c", 3))
m.Keys() // TreeSet("a", "b", "c")
m.Values() // [1, 2, 3] (sorted key order)
m.ToArray() // Array(("a",1), ("b",2), ("c",3))
m.ToGoMap() // map[string]int{"a":1, "b":2, "c":3}
m.ToHashMap() // HashMap(a -> 1, b -> 2, c -> 3)
m.String() // "TreeMap(a -> 1, b -> 2, c -> 3)"
m.MkString(", ") // "(a, 1), (b, 2), (c, 3)"
| Use Case | Recommended |
|---|---|
| Stack operations (push/pop from front) | List or Array |
| Random access by index | Array |
| Frequent appends to end | Array |
| Frequent prepends to front | List or Array |
| Recursive algorithms on sequences | List |
| Large sequences with updates | Array |
| Fast membership testing | HashSet |
| Set operations (union, intersection) | HashSet or TreeSet |
| Unique elements collection | HashSet or TreeSet |
| Sorted iteration needed | TreeSet or TreeMap |
| Need min/max of set | TreeSet |
| Range queries (elements between X and Y) | TreeSet or TreeMap |
| Key-value lookup (unordered) | HashMap |
| Key-value lookup (sorted keys) | TreeMap |
| Sorted key-value iteration | TreeMap |
| Min/max key access | TreeMap |
| Range queries on keys | TreeMap |
Note: With the prefix buffer optimization, Array now has O(1) amortized prepend, making it competitive with List for prepend-heavy workloads. Choose List when you need true O(1) without amortization, or Array when you also need random access.
| Need | Choose |
|---|---|
| Fastest put/get/remove | HashMap (O(eC) vs O(log n)) |
| Entries in sorted key order | TreeMap |
| Min/Max key access | TreeMap (O(log n) vs O(n)) |
| Range queries on keys | TreeMap |
| Key order doesn't matter | HashMap |
- O(1) prepend (cons)
- O(1) head and tail access
- Natural for recursive algorithms
- Efficient structural sharing for immutability
- O(eC) random access
- O(eC) append
- O(1) amortized prepend (using prefix buffer)
- O(eC) update at any position
- Better cache locality for iteration
- O(eC) add, remove, and contains operations (faster than TreeSet)
- Efficient set operations (union, intersection, difference)
- No duplicate elements
- Works with any
comparabletype - Best choice when order doesn't matter
- O(log n) add, remove, and contains operations
- Elements maintained in sorted order
- O(log n) min/max access
- Range queries (elements in [from, to])
- Sorted iteration guaranteed
- Best choice when you need ordering or range operations
List is implemented as a classic persistent linked list (cons list). Each node contains a value and a pointer to the tail. This provides:
- Structural sharing: prepending creates a new node pointing to the existing list
- Cached length for O(1) size queries
Array is implemented as a 32-way branching trie (similar to Scala's Vector and Clojure's PersistentVector) with several Scala-inspired optimizations:
- Tree depth of at most 7 for up to 34 billion elements
- Path copying for updates, sharing unaffected subtrees
- Effectively constant time operations (O(log32 n))
- Prefix buffer: prepended elements are stored in a separate buffer until it reaches 32 elements, then consolidated (O(1) amortized prepend)
HashSet is implemented as a Hash Array Mapped Trie (HAMT), similar to Scala's HashSet:
- 32-way branching trie with bitmap indexing
- Hash values determine path through the trie
- Collision handling at leaf nodes (when max depth reached)
- Path copying for updates, sharing unaffected subtrees
- Effectively constant time operations (O(log32 n))
TreeSet is implemented as a persistent Red-Black tree, similar to Scala's TreeSet:
- Self-balancing binary search tree with red/black coloring
- Height guaranteed to be at most 2*log(n+1)
- Path copying for updates, sharing unaffected subtrees
- O(log n) operations with in-order traversal for sorted iteration
- Supports range queries by exploiting tree structure
HashMap is implemented as a Hash Array Mapped Trie (HAMT), similar to Scala's HashMap:
- Same HAMT structure as HashSet but stores key-value pairs
- Effectively constant time operations (O(log32 n))
- Path copying for updates
TreeMap is implemented as a persistent Red-Black tree, similar to Scala's TreeMap:
- Same Okasaki-style functional Red-Black tree as TreeSet but stores key-value pairs
- O(log n) operations with in-order traversal for sorted key iteration
- Range queries by exploiting tree structure (Range, RangeFrom, RangeTo)
- Keys() returns a TreeSet for efficient sorted key access
Benchmark results comparing GALA immutable collections to Go native slices (immutable style with copy-on-write).
# GALA immutable collections benchmark
bazel run //collection_immutable:perf_gala
# Go native slices benchmark (immutable style)
bazel run //collection_immutable:perf_go| Operation | GALA List | GALA Array | Go Slice (immutable) |
|---|---|---|---|
| Creation | 129,766 | 3,670,004 | 34,890,000 |
| Prepend | 0 | 0 | 5,229 |
| Append | - | 445 | 7,443 |
| Head | 1 | 5 | 1 |
| Get(5000) | 4,088 | 5 | 0 |
| Update(5000) | - | 544 | 7,337 |
| Filter | 169,000 | 78,000 | 15,463 |
| Map | 265,000 | 114,000 | 10,476 |
| FoldLeft | 9,527 | 41,000 | 1,039 |
| Take(5000) | - | 54,000 | 515 |
| Drop(5000) | - | 52,000 | 1,044 |
| Operation | HashSet | TreeSet |
|---|---|---|
| Creation | 6,860,911 | 11,040,081 |
| Add | 1,078 | 1,218 |
| Contains (hit) | 25 | 707 |
| Contains (miss) | 44 | 952 |
| Remove | 1,430 | 1,932 |
| Min | O(n) | 17 |
| Max | O(n) | 27 |
| Filter | 6,470,311 | 12,195,500 |
| Operation | HashSet | TreeSet |
|---|---|---|
| Union | 678,951 | 1,353,863 |
| Intersect | 500,559 | 1,196,271 |
| Range | O(n) | 12,695,384 |
| Operation | 100 elements | 10,000 elements | 100,000 elements |
|---|---|---|---|
| List.Creation | 2,067 ns | 129,766 ns | 1,239,000 ns |
| Array.Creation | 17,011 ns | 3,670,004 ns | 52,193,000 ns |
| Operation | 100 elements | 10,000 elements | 100,000 elements |
|---|---|---|---|
| HashSet.Creation | 34,120 ns | 6,860,911 ns | 174,706,970 ns |
| TreeSet.Creation | 81,696 ns | 11,040,081 ns | 345,415,150 ns |
| Operation | HashMap | TreeMap |
|---|---|---|
| Creation | 8,224,307 | 12,037,826 |
| Put | 1,170 | 1,767 |
| Get (hit) | 25 | 368 |
| Get (miss) | 27 | 470 |
| Contains (hit) | 35 | 368 |
| Contains (miss) | 22 | 480 |
| Remove | 816 | 1,215 |
| MinKey | O(n) | 15 |
| MaxKey | O(n) | 19 |
| Filter | 3,678,677 | 7,988,481 |
| MapValues | 7,174,073 | 17,429,089 |
| PutAll (2x1000) | 754,050 | 1,092,953 |
| Operation | 100 elements | 10,000 elements | 100,000 elements |
|---|---|---|---|
| HashMap.Creation | 35,016 ns | 8,224,307 ns | 112,375,790 ns |
| TreeMap.Creation | 30,668 ns | 12,037,826 ns | 202,606,830 ns |
Notes:
- GALA List uses Prepend (O(1)), GALA Array uses Append (O(eC)), GALA HashSet uses Add (O(eC))
- Go Slice (immutable): copy-on-write style, full copy on each modification
-indicates operation not measured or not the primary use case- Array uses optimized bulk building for Filter, Map, Take, Drop operations
- HashSet/HashMap Contains is O(eC) regardless of collection size
List Strengths:
- O(1) Prepend (1 ns): Fastest way to build collections from the front
- O(1) Head/Tail: Efficient for stack-like patterns
- Linear Creation: Scales well (10x elements ≈ 10x time)
Array Strengths:
- O(eC) Prepend (0 ns): Amortized constant time using prefix buffer (Scala-inspired)
- O(eC) Append (460 ns): 16x faster than immutable slice copy
- O(eC) Get (5 ns): Constant random access regardless of position
- O(eC) Update (544 ns): 14x faster than immutable slice copy
HashSet Strengths:
- O(eC) Contains (25 ns): Fast membership testing regardless of set size
- O(eC) Add (1,078 ns): Efficient element insertion
- O(eC) Remove (1,430 ns): Efficient element removal
- Set operations: Union, intersection, difference in O(m) time (m = smaller set)
TreeSet Strengths:
- Sorted order: Elements always maintained in sorted order
- O(log n) Min/Max (17-27 ns): Instant access to smallest/largest elements
- Range queries: Efficient retrieval of elements in a range
- Sorted iteration: ForEach, ToSlice, etc. all produce sorted output
HashMap Strengths:
- O(eC) Get (25 ns): Fast key-value lookup regardless of map size
- O(eC) Put (1,170 ns): Efficient insertion
- O(eC) Remove (816 ns): Efficient removal
- Fastest map operations: ~15x faster Get than TreeMap
TreeMap Strengths:
- Sorted key order: Entries maintained in sorted key order
- O(log n) MinKey/MaxKey (15-19 ns): Instant access to smallest/largest keys
- Range queries: Efficient retrieval of entries in a key range
- Sorted iteration: ForEachKV, ToArray, etc. all produce sorted output
When to Choose HashMap vs TreeMap:
| Need | Choose |
|---|---|
| Fastest get/put/remove | HashMap (O(eC) vs O(log n)) |
| Entries in sorted key order | TreeMap |
| Min/Max key access | TreeMap (O(log n) vs O(n)) |
| Range queries on keys | TreeMap |
| Key order doesn't matter | HashMap |
When to Choose HashSet vs TreeSet:
| Need | Choose |
|---|---|
| Fastest contains/add/remove | HashSet (O(eC) vs O(log n)) |
| Elements in sorted order | TreeSet |
| Min/Max of set | TreeSet (O(log n) vs O(n)) |
| Range queries | TreeSet |
| Order doesn't matter | HashSet |
Comparison to Go Immutable Slices:
| Operation | GALA Array | Go Slice (copy) | GALA Advantage |
|---|---|---|---|
| Creation(10k) | 3.7ms | 34.9ms | 9x faster |
| Prepend | 0 ns | 5,229 ns | ∞ faster (O(1) vs O(n)) |
| Append | 445 ns | 7,443 ns | 17x faster |
| Update | 544 ns | 7,337 ns | 13x faster |
| Get | 5 ns | 0 ns | ~same |
When to Use Each:
| Scenario | Recommendation |
|---|---|
| Stack operations (LIFO) | List |
| Queue-like building (append) | Array |
| Random access needed | Array |
| Frequent updates | Array |
| Large collections with sharing | Array |
| Recursive algorithms | List |
| Fast membership testing | HashSet |
| Set operations (union, intersect) | HashSet (fastest) or TreeSet (sorted) |
| Unique elements only | HashSet or TreeSet |
| Need sorted set | TreeSet |
| Need min/max of set | TreeSet |
| Need range queries | TreeSet or TreeMap |
| Fast key-value lookup | HashMap |
| Sorted key-value map | TreeMap |
| Min/max key of map | TreeMap |
| Simple iteration only | Go slice |
package main
import . "martianoff/gala/collection_immutable"
func main() {
// Build a list of numbers
val numbers = ListOf(1, 2, 3, 4, 5)
// Transform: double each number
val doubled = numbers.Map((x) => x * 2)
// Filter: keep only values > 5
val filtered = doubled.Filter((x) => x > 5)
// Reduce: sum all values
val sum = filtered.Reduce((a, b) => a + b)
Println(s"Sum: $sum") // Sum: 24 (6 + 8 + 10)
// Original list is unchanged
Println(s"Original: ${numbers.String()}") // List(1, 2, 3, 4, 5)
}
package main
import . "martianoff/gala/collection_immutable"
func main() {
// Build array of 1000 elements
var arr Array[int] = EmptyArray[int]()
for i := 0; i < 1000; i = i + 1 {
arr = arr.Append(i * i)
}
// Random access - O(eC)
Println(s"Element at 500: ${arr.Get(500)}")
// Update - O(eC)
val updated = arr.Updated(500, 999999)
Println(s"Updated element at 500: ${updated.Get(500)}")
// Original unchanged
Println(s"Original element at 500: ${arr.Get(500)}")
}