Elixir: Getting Started with Functional Programming
Elixir is a dynamic, functional programming language built on the Erlang Virtual Machine (BEAM). It combines Ruby-inspired syntax with battle-tested concurrency primitives, making it ideal for building scalable, fault-tolerant systems.
In this tutorial, you’ll learn Elixir’s core concepts: functional programming with immutability, pattern matching, the pipe operator, the Mix build tool, the BEAM VM architecture, and a quick look at the Phoenix web framework.
What You’ll Learn
- Elixir’s relationship with the BEAM VM and Erlang ecosystem
- Functional programming: immutability, pure functions, recursion
- Pattern matching — Elixir’s superpower
- The pipe operator (
|>) for data transformation pipelines - Mix build tool for project management
- OTP (Open Telecom Platform) and concurrency
- Phoenix web framework introduction
Why Elixir Matters
Elixir powers high-traffic systems like Discord’s voice infrastructure, Pinterest’s real-time notifications, and Bleacher Report’s mobile APIs. At DodaTech, we evaluate Elixir for real-time collaboration features in DodaZIP where concurrent file operations must be fault-tolerant.
Learning Path
flowchart LR
A[Functional Programming] --> B[Elixir Basics<br/>You are here]
B --> C[Pattern Matching]
C --> D[Mix & OTP]
D --> E[Phoenix Framework]
style B fill:#f90,color:#fff
The BEAM VM
The BEAM (Bogdan’s Erlang Abstract Machine) is Elixir’s runtime. It provides:
| Feature | What It Means |
|---|---|
| Processes | Lightweight (microseconds to spawn, 1KB memory each) |
| Message passing | Share nothing — communicate via messages |
| Supervision trees | Restart failed components automatically |
| Hot code swapping | Update code without stopping the system |
Millions of processes can run concurrently on the BEAM. Compare this to OS threads (MB each) or goroutines (KB each). Elixir processes are measured in bytes.
Pattern Matching
In most languages, = is assignment. In Elixir, it’s a match operator:
# Basic matching
x = 42 # Binds 42 to x
42 = x # 42 = 42 -> true
# 43 = x # MatchError! 43 != 42
# Tuple matching
{status, value} = {:ok, "hello"}
IO.puts(status) # ok
IO.puts(value) # hello
# List matching
[head | tail] = [1, 2, 3, 4]
IO.puts(head) # 1
IO.inspect(tail) # [2, 3, 4]
# Pin operator — match against existing value
name = "Alice"
# ^name = "Bob" # MatchError — ^name pins to "Alice"Expected output: Pattern matching destructures data and assigns variables. The pin operator ^ forces matching against an existing value instead of rebinding.
Immutability
Variables in Elixir are immutable. Instead of modifying data, you create new data:
# Immutability example
list = [1, 2, 3]
new_list = [0 | list] # Prepend, don't modify
IO.inspect(list) # [1, 2, 3] (unchanged)
IO.inspect(new_list) # [0, 1, 2, 3]
# Update a map
user = %{name: "Alice", age: 30}
updated = %{user | age: 31}
IO.inspect(user) # %{name: "Alice", age: 30} (unchanged)
IO.inspect(updated) # %{name: "Alice", age: 31}Expected output: The original data is never modified. New data structures share memory with the originals via structural sharing (efficient).
The Pipe Operator
The pipe operator (|>) passes the result of one function as the first argument to the next. It creates readable data transformation pipelines:
# Without pipes — nested, hard to read
result = Enum.sort(Enum.filter(Enum.map(list, fn x -> x * 2 end), fn x -> x > 5 end))
# With pipes — reads top-to-bottom
result = list
|> Enum.map(fn x -> x * 2 end)
|> Enum.filter(fn x -> x > 5 end)
|> Enum.sort()
IO.inspect(result) # [6, 8, 10] (assuming list = [1,2,3,4,5])Expected output: [6, 8, 10]. The pipe chains operations: map multiplies by 2, filter keeps values > 5, sort orders ascending.
Mix Build Tool
Mix is Elixir’s build tool — it handles project creation, dependencies, testing, and more:
# Create a new project
mix new my_project
# Output:
# * creating README.md
# * creating lib/my_project.ex
# * creating test/my_project_test.exs
# * creating mix.exs
# Run tests
mix test
# Add dependency (in mix.exs)
# defp deps do
# [{:phoenix, "~> 1.7"}]
# end
# Install dependencies
mix deps.get
# Run interactive shell
iex -S mix# lib/my_project.ex
defmodule MyProject do
@moduledoc """
Documentation for MyProject.
"""
@doc """
Greets a user.
"""
def greet(name) do
"Hello, #{name}!"
end
end# Usage
iex -S mix
iex(1)> MyProject.greet("Alice")
# "Hello, Alice!"Expected output: The mix new command scaffolds a complete project structure. iex -S mix loads the project into an interactive shell.
OTP and Concurrency
OTP (Open Telecom Platform) provides abstractions for concurrent programming:
# Spawn a process
defmodule Greeter do
def greet do
receive do
{:hello, name} ->
IO.puts("Hello, #{name}!")
greet() # Loop to handle more messages
end
end
end
# In iex
pid = spawn(&Greeter.greet/0)
send(pid, {:hello, "Alice"})
# Hello, Alice!
send(pid, {:hello, "Bob"})
# Hello, Bob!Expected output: Each send delivers a message to the process. The process receives and handles messages sequentially in its mailbox. Durga Antivirus Pro uses similar actor-model patterns for concurrent file scanning.
Phoenix Web Framework
Phoenix is Elixir’s flagship web framework — think Ruby on Rails performance on steroids:
# Router (lib/my_app_web/router.ex)
defmodule MyAppWeb.Router do
use MyAppWeb, :router
pipeline :api do
plug :accepts, ["json"]
end
scope "/api", MyAppWeb do
pipe_through :api
get "/users", UserController, :index
post "/users", UserController, :create
end
endPhoenix Channels provide real-time bidirectional communication via WebSocket — powering chat, notifications, and live updates.
Common Mistakes
1. Forgetting the Pin Operator in Pattern Matching
Using = inside a case or function head rebinds the variable. Use ^ to match against an existing value.
2. Modifying Data Instead of Transforming
You can’t mutate a list or map. Always create new data structures. The old ones remain untouched.
3. Using Loops Instead of Recursion
Elixir has no for/while loops. Use recursion and Enum functions (map, reduce, filter) instead.
4. Not Using the Pipe Operator
Nested function calls are hard to read. Use |> to create readable left-to-right pipelines.
5. Ignoring OTP Supervision
Spawning bare processes without supervision means crashes go unhandled. Always use OTP supervisors.
6. Blocking in a Process
A long-running synchronous operation blocks the process’s mailbox. Use Task.async for heavy computations.
Practice Questions
1. What is pattern matching in Elixir?
= is a match operator, not assignment. If the left side matches the right side, variables are bound. If not, a MatchError is raised.
2. What does the pipe operator do?
It passes the result of the left expression as the first argument to the function on the right, creating readable data transformation pipelines.
3. What is a BEAM process?
A lightweight unit of concurrency (not an OS thread). Processes share nothing and communicate via message passing.
4. What is Mix used for?
Project creation, dependency management, testing, compilation, and running tasks.
5. Challenge: Build a simple GenServer.
Create a GenServer that maintains a counter. Implement increment, decrement, and get_value calls.
Mini Project: URL Fetcher
Build a program that fetches multiple URLs concurrently:
defmodule URLFetcher do
def fetch(url) do
Task.async(fn ->
{:ok, body} = HTTPoison.get(url)
String.length(body)
end)
end
def run(urls) do
tasks = Enum.map(urls, &fetch/1)
Task.await_many(tasks, :infinity)
end
end
# Usage
# URLFetcher.run(["https://elixir-lang.org", "https://hex.pm"])FAQ
What’s Next
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro