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.

Create a free account to save this page, take notes and track your progress. Sign in

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 allow map.name). Update with %{map | name: "Ben"} (existing keys) or Map.put.
  • Structs defstruct [:name, score: 0]: maps with a fixed set of fields and a name, checked at compile time.
  • Enum functions work on any enumerable: map, filter, reduce, sum, sort_by, group_by, frequencies, take. Stream is 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

Structs, Enum pipelines, comprehensions and recursion
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 use Enum functions.
  • Indexing lists with Enum.at in loops; lists aren't arrays. Use maps or tuples for random access.
  • map.key on string-keyed maps. Dot access only works for atom keys; use map["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).
  • Enum for eager transformations, Stream for lazy ones.
  • for comprehensions 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.

Was this page helpful?