Mutability & Persistent Data Structures
Nostos uses persistent data structures - Lists, Maps, and Sets are immutable. Operations like insert or append return a new structure, leaving the original unchanged. Combined with mutable bindings, this gives you the best of both worlds: the safety of immutability with the convenience of imperative-style updates.
Immutable vs Mutable Bindings
Quick recap: bindings are immutable by default. Use var for mutable bindings.
# Immutable binding
x = 10
# x = 20 # ERROR: Can't rebind to different value
# Mutable binding
var count = 0
count = count + 1 # OK
count = count + 1 # OK
println(count) # 2
Persistent Data Structures
Lists, Maps, and Sets in Nostos are persistent (immutable). When you "modify" them, you get a new structure - the original remains unchanged.
original = [1, 2, 3]
modified = original ++ [4] # Append returns a NEW list
println(original) # [1, 2, 3] - unchanged!
println(modified) # [1, 2, 3, 4]
# Same for Maps
map1 = %{"a": 1}
map2 = map1.insert("b", 2) # Returns a NEW map
println(map1) # %{"a": 1} - unchanged!
println(map2) # %{"a": 1, "b": 2}
Why persistent? Persistent structures are inherently thread-safe - no locks needed when sharing between processes. They also make debugging easier since values never change unexpectedly.
The Pattern: Mutable Var + Persistent Structure
The idiomatic way to build up a collection is to use a mutable binding that holds successive versions of a persistent structure:
# Building a list with a mutable binding
main() = {
var items = []
items = items ++ [1]
items = items ++ [2]
items = items ++ [3]
println(items) # [1, 2, 3]
}
The same pattern works for Maps and Sets:
# Building a map incrementally
main() = {
var config = %{}
config = config.insert("debug", true)
config = config.insert("port", 8080)
config = config.insert("host", "localhost")
println(config)
# %{"debug": true, "port": 8080, "host": "localhost"}
}
# Building a set
main() = {
var seen = #{}
seen = seen.insert("apple")
seen = seen.insert("banana")
seen = seen.insert("apple") # Duplicate - no effect
println(seen) # #{"apple", "banana"}
}
Building Collections in Loops
This pattern shines in loops where you accumulate results:
# Collect squares of numbers
main() = {
var squares = []
for i = 1 to 5 {
squares = squares ++ [i * i]
}
println(squares) # [1, 4, 9, 16, 25]
}
# Count word frequencies
count_words(words) = {
var counts = %{}
words.each(word => {
current = counts.get(word).unwrapOr(0)
counts = counts.insert(word, current + 1)
})
counts
}
main() = {
words = ["apple", "banana", "apple", "cherry", "banana", "apple"]
println(count_words(words))
# %{"apple": 3, "banana": 2, "cherry": 1}
}
Comparison: Imperative vs Functional
Both styles are valid. Use whichever fits your situation:
Mutable var + loop
var result = []
for i = 1 to 5 {
result = result ++ [i * i]
}
result # [1, 4, 9, 16, 25]
Functional with map
[1, 2, 3, 4, 5].map(i => i * i)
# [1, 4, 9, 16, 25]
Mutable var accumulation
var total = 0
for x in [1, 2, 3, 4, 5] {
total = total + x
}
total # 15
Functional with fold
[1, 2, 3, 4, 5].fold(0, (acc, x) => acc + x)
# 15
When to Use Each Approach
Guidelines
- Use mutable vars when building collections incrementally over complex logic, or when translating imperative algorithms
- Use functional style (map, filter, fold) for simple transformations - it's often more concise
- Mix both freely - Nostos doesn't force you into one paradigm
- For shared state between processes, use
mvarinstead (see Concurrency chapter)
Note on Thread Safety
Mutable vars (var) are local to a single process and are not shared between processes. For shared mutable state across processes, use mvar (module-level mutable variables) which have automatic locking.
# Local mutable var - only visible to this process
main() = {
var local_count = 0
local_count = local_count + 1 # OK, single-process access
}
# Shared mvar - safe for concurrent access
mvar shared_count: Int = 0
increment() = {
shared_count = shared_count + 1 # Automatically locked
}
main() = {
# Spawn 100 processes all incrementing
for i = 1 to 100 {
spawn { increment() }
}
sleep(100)
println(shared_count) # 100 - no race conditions!
}