Elixir: Data & Immutability

Every value in Elixir is immutable: functions return new data rather than modifying what they receive. This lesson tours the built-in types and then explains the one idea — immutability — that makes the rest of the language safe to run in parallel.

Immutability and Rebinding

In most languages, assigning to a variable again replaces the value in a box. Elixir has no boxes: a variable is a label bound to a value, and rebinding binds the label to a different value. The old value itself is untouched — any other code holding it still sees the original.

Rebinding a variable creates a new value while the old value remains shared and unchanged
Fig. 1 — map = %{a: 1} then Map.put(map, :b, 2) leaves both maps valid and independent.
map = %{a: 1}
bigger = Map.put(map, :b, 2)   # returns a NEW map

map.a                          #=> 1  (unchanged — safe to share)
bigger.b                       #=> 2

# The update syntax rebinds an existing variable; it reads like mutation
# but is a fresh structure underneath.
counter = %{hits: 0}
counter = %{counter | hits: 1}  #  |  requires the key to already exist
counter = Map.update(counter, :hits, 0, &(&1 + 1))  #=> %{hits: 2}

Two practical consequences. First, sharing data between processes is safe with no locks — nobody can change a shared value behind your back. Second, updates stay fast: Elixir's structures are persistent data structures, so a "new" map shares most of its internals with the old one instead of copying everything.

Numbers

1_000_000      # underscores for readability — integers are arbitrary precision
0x1F           # hex literals (0b binary, 0o octal too)
10 / 3         #=> 3.3333333333333335  — / ALWAYS returns a float
div(10, 3)     #=> 3   — integer division
rem(10, 3)     #=> 1   — remainder
0.1 + 0.2      #=> 0.30000000000000004 — floats are IEEE 754 doubles

Atoms and Booleans

An atom is a constant whose name is its value: :ok, :error, :transient. They are never garbage-collected, so never build them dynamically from user input. true, false, and nil are just the atoms :true, :false, and :nil in disguise.

:ok == :ok                         #=> true
is_atom(true)                      #=> true — booleans ARE atoms
String.to_atom("dyn_" <> "suffix") # legal, but a memory leak if driven by input

Tuples vs Lists

Tuples and lists look similar in code and behave very differently in memory. Choosing correctly is one of the first habits of idiomatic Elixir.

Tuple {...}List [...]
Storagecontiguous arraysingly linked cells
Access by indexO(1) — elem(t, 0)O(n)
PrependO(n) — rebuildsO(1) — [h | t]
Lengthknown at constructionO(n) to count
Typical usefixed-shape returns: {:ok, value}variable-length collections to enumerate
status = {:ok, "written"}      # tuple: a fixed shape, read as a whole
list = [3, 1, 2]               # list: built for head/tail recursion
[head | tail] = list           # head = 3, tail = [1, 2]
Enum.sum(list)                 #=> 6

Keyword Lists and Maps

A keyword list is a list of {atom, value} pairs — ordered, keys may repeat, and it doubles as the syntax for function options. A map is the general key/value store: unordered, unique keys, pattern-matchable, with O(log n) access for large maps.

opts = [timeout: 5_000, retries: 3]  # keyword list
opts[:timeout]                       #=> 5000
# The bracket sugar appears in real APIs everywhere:
Req.get("https://sagecode.org", retry: :safe, max_retries: 2)

map = %{"name" => "Ada", role: :admin}  # string or atom keys
map.role                                 #=> :admin — atom-key shorthand
%{role: r} = map                         # pattern matching works on maps
Map.put(map, :visits, 1)                 # functional update returns a new map

Structs and Dates

A struct is a map with a guaranteed shape: it names a module, rejects unknown keys at compile time, and enables protocols and pattern matching on type. The standard library itself models time this way.

defmodule Booking do
  defstruct [:title, :seats]      # enforced shape, defaults to nil
end

b = %Booking{title: "Intro", seats: 30}
%Booking{seats: seats} = b        # matches only Booking structs
seats                             #=> 30

~D[2026-09-17]                    # Date struct via sigil
Date.add(~D[2026-09-17], 3)       #=> ~D[2026-09-20]

Practice

  1. Rebind a map three times, printing it at each step, and explain what happened to the earlier bindings.
  2. Write a function summarize({:ok, value}) / summarize({:error, reason}) and test both clauses.
  3. Convert a keyword list of options into a map and back; note which operations each structure makes easy.

Next: Pattern Matching