Immutable Data Structures in Elixir: Why They Matter
When you’re getting into Elixir—or functional programming in general—you’ll quickly hear about immutability. I’ve previously talked about how it’s one of the core fundamental concepts of Elixir, so in this post, I’ll explore:
- What immutability actually means
- How Elixir handles memory when variables “change”
- How lists, tuples, and maps work (and which one you’ll likely use the most)
- Why this whole thing is useful in the real world
What Does “Immutable” Mean, Anyway?
At its core, immutability means you can’t change data once it’s created. Instead, if you want to “change” something, you create a new version of that data with your updates.
Here’s a quick Elixir example:
1
2
3
4
5
name = "Alex"
new_name = name <> " Raza"
IO.puts(name) # => "Alex"
IO.puts(new_name) # => "Alex Raza"
Here’s what’s happening under the hood: even though I reused the variable name name
when writing new_name = name <> " Raza"
, Elixir doesn’t change the original string. Instead, it allocates new memory to store the new value ("Alex Raza"
).
If you later do this:
1
name = "Kiptoo"
You’re not changing the original value. You’re binding the variable name
to a new value. The old value ("Alex"
) is still intact and untouched (though garbage collection may clean it up if nothing else references it).
So Elixir’s approach to variables is really more about binding names to values than assigning values to variables.
Lists: The Functional Workhorse
Lists are ordered collections, and in Elixir, they are implemented as linked lists. That means adding to the front is super fast, but getting something from the end is a little more expensive.
Example:
1
2
3
4
5
names = ["Ali", "Mwangi", "Faith"]
updated = ["Jane" | names]
IO.inspect(updated)
# => ["Jane", "Ali", "Mwangi", "Faith"]
Notice how we added “Jane” to the front, creating a new list. names
is still safe and unchanged.
When to use lists:
- You work with collections of values where order matters.
- You do lots of prepending (not appending).
You’ll use lists a lot in Elixir. They’re great for logs, streams, user input queues, and data you want to display or traverse linearly.
Common Modules:
Enum
: For transforming, filtering, reducing, and walking through lists.List
: For operations like flattening, duplicating, deleting, etc.Stream
: For working with lazy (deferred) sequences, great for pipelines with large datasets.
Tuples: Fixed, Fast, and Often Overlooked
Tuples are collections with a fixed size and are indexed numerically, starting from 0.
Example:
1
2
3
4
5
coords = {5, 10}
new_coords = put_elem(coords, 0, 15)
IO.inspect(coords) # => {5, 10}
IO.inspect(new_coords) # => {15, 10}
Just like lists, tuples are immutable. We didn’t modify coords
; we created new_coords
.
When to use tuples:
- When returning multiple values from a function (e.g.,
{:ok, result}
or{:error, reason}
). - When data has a fixed shape and size.
Tuples are great for returning function results, error handling, and lightweight data structures. But you won’t use them as often as lists or maps.
Maps: Your Best Bet for Key-Value Data
Maps are collections of key-value pairs and are arguably the most versatile structure in Elixir.
Example:
1
2
3
4
5
user = %{name: "Wanjiku", age: 24}
updated_user = %{user | age: 25}
IO.inspect(updated_user)
# => %{name: "Wanjiku", age: 25}
Like with the others, the original user
map is untouched.
When to use maps:
- For modeling entities (users, products, settings, etc.).
- When keys are strings or atoms and order doesn’t matter.
Maps are likely what you’ll use the most in day-to-day Elixir. They’re flexible, easy to work with, and align perfectly with how data is structured in JSON, APIs, and configuration files.
Common Modules:
Map
: For merging, updating, filtering maps.Keyword
: Special kind of map for atom-keyed lists.
Why This All Matters
1. No Unexpected Changes
When your variables are just bindings to values that never change, your data is safer. It prevents side effects, making it easier to understand what your code is doing.
2. Safe for Concurrency
Elixir thrives in concurrent environments. Immutability ensures processes don’t clash over shared data. Everyone has their own copy—no stepping on toes.
3. Easier Debugging
You can always trust that once a value is set, it stays the same. If something goes wrong, you don’t have to wonder where in the code it got mutated.
Final Thoughts
Immutable data structures in Elixir aren’t just a programming constraint, they’re a feature that gives you:
- Cleaner, safer code
- Better reasoning and testing
- Concurrency that just works
Use lists for ordered sequences, tuples for fixed groupings, and maps for structured data you want to label.