Elixir Fundamentals: Functional Programming and OTP · Functional Elixir: Data, Pattern Matching and Functions
Lists, Maps, Structs, Enum and Recursion
Work with lists, keyword lists, maps and structs; transform them with Enum (map, filter, reduce, group_by, sort_by) and comprehensions; and understand recursion, the functional replacement for loops.
What you'll build
A small grade book: students stored as structs, statistics with Enum, grouping by grade, a for comprehension, and a recursive function that sums a list the "manual" way.
The idea
- Lists
[1, 2, 3]: linked lists; prepending ([0 | list]) is fast, indexing is slow. - Keyword lists
[timeout: 5000, retries: 3]: lists of{atom, value}pairs, used for options. - Maps
%{"ana" => 90}or%{name: "Ana"}(atom keys allowmap.name). Update with%{map | name: "Ben"}(existing keys) orMap.put. - Structs
defstruct [:name, score: 0]: maps with a fixed set of fields and a name, checked at compile time. Enumfunctions work on any enumerable:map,filter,reduce,sum,sort_by,group_by,frequencies,take.Streamis the lazy version for large or infinite data.- Comprehensions:
for x <- list, x > 0, do: x * 2. - Recursion replaces loops: a function handles the first element and calls itself on the rest, with a clause for the empty list.
The code
defmodule Student do
defstruct [:name, scores: []]
def average(%Student{scores: []}), do: 0.0
def average(%Student{scores: scores}), do: Enum.sum(scores) / length(scores)
end
defmodule MyList do
def sum([]), do: 0 # base case
def sum([head | tail]), do: head + sum(tail) # recursive case
end
# In a Mix project you'd write %Student{name: "Ana", ...}; a script that also defines the
# struct builds it with struct!/2, because the whole file is compiled before it runs.
students = [
struct!(Student, name: "Ana", scores: [92, 88, 95]),
struct!(Student, name: "Ben", scores: [71, 64, 80]),
struct!(Student, name: "Cleo", scores: [85, 91, 78]),
struct!(Student, name: "Dev", scores: [])
]
students
|> Enum.map(fn s -> {s.name, Float.round(Student.average(s), 1)} end)
|> Enum.sort_by(fn {_name, avg} -> avg end, :desc)
|> Enum.each(fn {name, avg} -> IO.puts("#{String.pad_trailing(name, 5)} #{avg}") end)
grade = fn avg ->
cond do
avg >= 90 -> :a
avg >= 80 -> :b
avg >= 70 -> :c
true -> :incomplete
end
end
students
|> Enum.group_by(&grade.(Student.average(&1)), & &1.name)
|> Enum.sort()
|> IO.inspect(label: "by grade")
all_scores = Enum.flat_map(students, & &1.scores)
IO.inspect(Enum.frequencies_by(all_scores, &div(&1, 10) * 10), label: "score bands")
honours = for s <- students, Student.average(s) >= 85, do: String.upcase(s.name)
IO.inspect(honours, label: "honours")
ben = Enum.find(students, &(&1.name == "Ben"))
improved = %{ben | scores: ben.scores ++ [95]} # a new struct; `ben` is unchanged
IO.inspect({length(ben.scores), length(improved.scores)}, label: "scores before/after")
IO.inspect(MyList.sum([1, 2, 3, 4]), label: "recursive sum")
IO.inspect(Stream.iterate(1, &(&1 * 2)) |> Enum.take(8), label: "lazy powers of two")
Output
Ana 91.7
Cleo 84.7
Ben 71.7
Dev 0.0
by grade: [a: ["Ana"], b: ["Cleo"], c: ["Ben"], incomplete: ["Dev"]]
score bands: %{60 => 1, 70 => 2, 80 => 3, 90 => 3}
honours: ["ANA"]
scores before/after: {3, 4}
recursive sum: 10
lazy powers of two: [1, 2, 4, 8, 16, 32, 64, 128]Run this on your computer — see “Get Started” for setup.
Common mistakes
- Appending in loops (
list ++ [x]) is O(n) each time; prepend and reverse, or useEnumfunctions. - Indexing lists with
Enum.atin loops; lists aren't arrays. Use maps or tuples for random access. map.keyon string-keyed maps. Dot access only works for atom keys; usemap["key"].- Forgetting the base case in recursion, which recurses until the process runs out of memory.
Key points
- Lists (prepend-friendly), keyword lists (options), maps, structs (named maps with fixed fields).
Enumfor eager transformations,Streamfor lazy ones.forcomprehensions with filters.- Recursion: a base case plus a clause on
[head | tail].
Further reading: Lists and tuples, Enum, Structs.
Practice questions
What's the difference between Enum and Stream in Elixir?
Answer
Enum functions are eager: each step builds a full intermediate collection. Stream functions are lazy: they build a recipe that runs only when consumed (e.g. by Enum.take or Enum.to_list), which suits large or infinite data.
How do you update a struct field in Elixir?
Answer
With the update syntax %{struct | field: new_value}, which returns a new struct (the original is unchanged). It only works for existing fields, which structs guarantee.
Open in the app to mark lessons complete, take notes and track your progress.