Starting Elixir - Part 2: Enum, Exercises & Processes
In Part 1 we covered the language fundamentals: pattern matching, immutability, the pipe operator, functions, and recursion. Now let’s put those tools to work and tackle the topics that make Elixir worth learning in the first place — ergonomic data processing with Enum, and concurrency with processes and tasks.
Enum — The Workhorse
In Part 1 we wrote recursive functions to work with lists. In practice, you rarely need to do that because the Enum module covers almost everything.
map — transform every element:
1
2
3
4
5
6
7
8
numbers = [8, 3, 15, 6, 21, 14]
Enum.map(numbers, fn x -> x * 2 end)
# => [16, 6, 30, 12, 42, 28]
# shorthand with the capture operator
Enum.map(numbers, &(&1 * 2))
# => [16, 6, 30, 12, 42, 28]
filter — keep elements that match a condition:
1
2
Enum.filter(numbers, fn x -> rem(x, 3) == 0 end)
# => [3, 15, 6, 21]
reduce — fold a list into a single value:
1
2
3
4
5
6
Enum.reduce(numbers, 0, fn x, acc -> acc + x end)
# => 67
# shorthand for operators
Enum.reduce(numbers, 0, &+/2)
# => 67
frequencies — count occurrences:
1
2
Enum.frequencies(["a", "b", "a", "c", "b", "a"])
# => %{"a" => 3, "b" => 2, "c" => 1}
group_by — group elements by a key function:
1
2
3
4
words = ["apple", "banana", "avocado", "blueberry", "apricot"]
Enum.group_by(words, &String.first/1)
# => %{"a" => ["apple", "avocado", "apricot"], "b" => ["banana", "blueberry"]}
Chaining them together with the pipe operator:
1
2
3
4
5
[8, 3, 15, 6, 21, 14]
|> Enum.filter(&(rem(&1, 3) == 0))
|> Enum.map(&(&1 * 10))
|> Enum.reduce(0, &+/2)
# => 450
Exercises
Let’s solve a few classic problems with what we know.
FizzBuzz
Print numbers 1 to 100, but replace multiples of 3 with "Fizz", multiples of 5 with "Buzz", and multiples of both with "FizzBuzz". Using anonymous function pattern matching inside Enum.map:
1
2
3
4
5
6
7
Enum.map(1..15, fn
x when rem(x, 15) == 0 -> "FizzBuzz"
x when rem(x, 3) == 0 -> "Fizz"
x when rem(x, 5) == 0 -> "Buzz"
x -> x
end)
# => [1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz", 11, "Fizz", 13, 14, "FizzBuzz"]
Word Frequency
Find the most common word in a sentence:
1
2
3
4
5
6
7
8
sentence = "the cat sat on the mat the"
sentence
|> String.downcase()
|> String.split()
|> Enum.frequencies()
|> Enum.max_by(fn {_word, count} -> count end)
# => {"the", 3}
Group Anagrams
Given a list of words, group together those that are anagrams of each other. The trick is to use the sorted characters as a grouping key:
1
2
3
4
5
6
words = ["eat", "tea", "tan", "ate", "nat", "bat"]
Enum.group_by(words, fn word ->
word |> String.graphemes() |> Enum.sort() |> Enum.join()
end)
# => %{"abt" => ["bat"], "aet" => ["eat", "tea", "ate"], "ant" => ["tan", "nat"]}
Real Data Processing
Let’s fetch real data and process it. We’ll use the Req package (same as Part 1) to pull GitHub repos for the elixir-lang org, then find the top 5 by star count:
1
2
3
4
5
6
7
8
9
10
11
{:ok, response} = Req.get("https://api.github.com/orgs/elixir-lang/repos?per_page=100")
response.body
|> Enum.map(fn repo -> %{name: repo["name"], stars: repo["stargazers_count"]} end)
|> Enum.sort_by(& &1.stars, :desc)
|> Enum.take(5)
# => [
# %{name: "elixir", stars: ...},
# %{name: "ex_doc", stars: ...},
# ...
# ]
That’s it — fetch, transform, sort, take. The pipe chain reads like a description of the steps.
Processes — Elixir’s Superpower
The first post mentioned that Elixir uses lightweight processes for concurrency but never showed them. Let’s fix that.
A process is spawned with spawn/1, receives messages with receive, and sends messages with send/2:
1
2
3
4
5
6
7
8
9
10
11
12
13
pid = spawn(fn ->
receive do
{:hello, sender} ->
send(sender, {:world, self()})
end
end)
send(pid, {:hello, self()})
receive do
{:world, _from} -> "process answered!"
end
# => "process answered!"
self() returns the PID of the current process — we pass it along so the spawned process knows where to reply.
Stateful Processes
Processes can hold state through recursion. Here is a simple counter:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
defmodule Counter do
def start(initial \\ 0) do
spawn(fn -> loop(initial) end)
end
defp loop(count) do
receive do
{:increment, from} ->
send(from, {:ok, count + 1})
loop(count + 1)
{:get, from} ->
send(from, {:ok, count})
loop(count)
:stop ->
:ok
end
end
end
pid = Counter.start()
send(pid, {:increment, self()})
receive do {:ok, v} -> v end
# => 1
send(pid, {:increment, self()})
receive do {:ok, v} -> v end
# => 2
send(pid, {:get, self()})
receive do {:ok, v} -> v end
# => 2
send(pid, :stop)
The state is never stored in a variable — it lives in the function argument, passed recursively on each message.
Note: In real Elixir applications you would use GenServer instead of writing this loop manually. GenServer wraps this exact pattern with error handling, supervision, and a cleaner API. But understanding the raw process model first makes GenServer much easier to read.
Task — Practical Concurrency
Raw processes are powerful but verbose. The Task module gives us a higher-level API for the common case: run work concurrently and collect the results.
1
2
3
4
5
6
7
task1 = Task.async(fn -> :timer.sleep(100); "result 1" end)
task2 = Task.async(fn -> :timer.sleep(100); "result 2" end)
task3 = Task.async(fn -> :timer.sleep(100); "result 3" end)
[Task.await(task1), Task.await(task2), Task.await(task3)]
# => ["result 1", "result 2", "result 3"]
# total time: ~100ms, not ~300ms
All three tasks run in parallel — the total time is roughly the time of the slowest task, not the sum.
A real-world example: fetch multiple URLs in parallel:
1
2
3
4
5
6
7
8
9
10
11
urls = [
"https://api.github.com/repos/elixir-lang/elixir",
"https://api.github.com/repos/phoenixframework/phoenix",
"https://api.github.com/repos/elixir-nx/nx"
]
urls
|> Enum.map(fn url -> Task.async(fn -> Req.get(url) end) end)
|> Enum.map(&Task.await(&1, 10_000))
|> Enum.map(fn {:ok, %{status: status}} -> status end)
# => [200, 200, 200]
Compare sequential vs parallel with :timer.tc/1:
1
2
3
4
5
6
7
8
9
10
11
12
{sequential_us, _} = :timer.tc(fn ->
Enum.map(urls, &Req.get/1)
end)
{parallel_us, _} = :timer.tc(fn ->
urls
|> Enum.map(&Task.async(fn -> Req.get(&1) end))
|> Enum.map(&Task.await(&1, 10_000))
end)
"Sequential: #{div(sequential_us, 1000)}ms — Parallel: #{div(parallel_us, 1000)}ms"
# => "Sequential: ~900ms — Parallel: ~320ms"
Task.async_stream — The Idiomatic Way
Manually calling Task.async + Task.await for each element works, but Elixir has a cleaner tool for the common case of mapping concurrently over a collection: Task.async_stream/3.
1
2
3
4
[1, 2, 3, 4, 5]
|> Task.async_stream(fn n -> n * n end)
|> Enum.map(fn {:ok, result} -> result end)
# => [1, 4, 9, 16, 25]
Each element produces an {:ok, result} tuple (or {:exit, reason} on failure), and the output preserves the input order. The previous URL fetch rewritten with async_stream:
1
2
3
4
5
6
7
urls
|> Task.async_stream(fn url -> Req.get(url) end,
max_concurrency: 3,
timeout: 10_000
)
|> Enum.map(fn {:ok, {:ok, %{status: status}}} -> status end)
# => [200, 200, 200]
The key difference from the manual approach is max_concurrency. When you spawn tasks with Task.async directly, all of them start immediately. With async_stream, at most max_concurrency tasks run at any moment — the rest wait in line. This is important when calling rate-limited APIs or doing I/O that you don’t want to flood:
1
2
3
4
# fetch 100 URLs, but only 10 at a time
many_urls
|> Task.async_stream(&Req.get/1, max_concurrency: 10, timeout: 30_000)
|> Enum.map(fn {:ok, {:ok, resp}} -> resp.status end)
In most real-world cases, Task.async_stream is the right choice over the manual Task.async + Task.await loop.
Conclusion
We have covered Enum for data transformation, solved real exercises idiomatically, processed data from a live API, and introduced the process model that powers Elixir’s concurrency story. From here, the natural next steps are:
- GenServer — the production-grade stateful process abstraction
- Supervisor — for fault-tolerant process trees
- Stream — lazy version of
Enumfor large or infinite data - Phoenix / LiveView — the web framework built on top of all of this
If you want to keep going, the Official Elixir guides are excellent and pick up exactly where this post leaves off.
Resources
- https://hexdocs.pm/elixir/Enum.html — Enum module docs
- https://hexdocs.pm/elixir/Task.html — Task module docs
- https://hexdocs.pm/elixir/introduction.html — Official Elixir introduction
- https://github.com/wojtekmach/req — Req HTTP client
