Quick Start
Ready to learn Elixir quickly? This quick start tutorial provides a fast-paced tour through Elixir’s core concepts. By the end, you’ll understand the fundamentals and be ready to build real applications.
Prerequisites
Before starting this tutorial, you need:
- Elixir 1.14+ installed (see Initial Setup)
- Basic programming knowledge (any language)
- Familiarity with command-line terminals
- Text editor of your choice
- 60-90 minutes of focused learning
Learning Objectives
By the end of this tutorial, you will understand:
- Pattern Matching - Elixir’s destructuring and matching mechanism
- Immutability - Data transformation without mutation
- Basic Types - Atoms, tuples, lists, maps, and structs
- Functions - Anonymous functions, named functions, and function clauses
- Modules - Code organization and namespacing
- Pipelines - Composing function calls with
|>operator - Recursion - Functional iteration without loops
- Enumerables - Working with collections using Enum and Stream
- Pattern Matching in Functions - Multiple function clauses
- Processes - Lightweight concurrency primitives
- Message Passing - Inter-process communication
- GenServer - OTP behavior for stateful servers
Learning Path
%%{init: {'theme':'base', 'themeVariables': { 'primaryColor':'#0ea5e9','primaryTextColor':'#1e293b','primaryBorderColor':'#0369a1','lineColor':'#64748b','secondaryColor':'#f97316','tertiaryColor':'#8b5cf6','background':'#ffffff','mainBkg':'#f1f5f9','secondBkg':'#e2e8f0'}}}%%
graph TB
Start[Quick Start] --> Setup[Prerequisites Check]
Setup --> Pattern[Pattern Matching]
Pattern --> Immut[Immutability]
Immut --> Types[Basic Types]
Types --> Funcs[Functions & Modules]
Funcs --> Pipeline[Pipeline Operator]
Pipeline --> Recur[Recursion]
Recur --> Enum[Enumerables]
Enum --> MultiClause[Multi-Clause Functions]
MultiClause --> Proc[Processes]
Proc --> Msg[Message Passing]
Msg --> Gen[GenServer]
Gen --> Next[Next Steps]
style Start fill:#0ea5e9,stroke:#0369a1,stroke-width:3px,color:#ffffff
style Next fill:#10b981,stroke:#059669,stroke-width:3px,color:#ffffff
style Pattern fill:#f97316,stroke:#c2410c,stroke-width:2px,color:#ffffff
style Proc fill:#8b5cf6,stroke:#6d28d9,stroke-width:2px,color:#ffffff
Pattern Matching Fundamentals
Pattern matching is Elixir’s most powerful feature - not just assignment, but structural comparison.
The Match Operator
The = operator is not assignment - it’s pattern matching:
iex
x = 42
xOutput:
42But this is pattern matching, not assignment. The left side must match the right side:
42 = x
43 = xOutput:
** (MatchError) no match of right hand side value: 42Destructuring with Tuples
Extract values from tuples:
{a, b, c} = {1, 2, 3}
a # => 1
b # => 2
c # => 3Common pattern for function returns:
{:ok, result} = {:ok, "Success!"}
result # => "Success!"
{:error, reason} = {:error, "File not found"}
reason # => "File not found"Pin Operator
Use ^ to match against existing value instead of rebinding:
x = 42
{x, y} = {43, "hello"}
x # => 43
x = 42
{^x, y} = {42, "hello"} # Works - 42 matches 42
y # => "hello"
{^x, y} = {43, "hello"} # Fails - 42 doesn't match 43Underscore for Ignoring Values
Use _ to ignore values you don’t need:
{:ok, _, message} = {:ok, 404, "Not found"}
message # => "Not found"
[head | _tail] = [1, 2, 3, 4, 5]
head # => 1Immutability and Data Transformation
Elixir data is immutable - values never change, only transform into new values.
Immutable Variables
original = [1, 2, 3]
modified = [0 | original]
original # => [1, 2, 3] - unchanged!
modified # => [0, 1, 2, 3] - new listVariables can be rebound (matched again), but values themselves never mutate:
x = 42
x = x + 1 # New binding, original 42 unchanged
x # => 43Benefits of Immutability
No side effects:
defmodule Calculator do
def add(a, b) do
a + b # Pure function - no mutations
end
end
result = Calculator.add(2, 3) # => 5Safe concurrency - multiple processes can access same data without locks.
Easier reasoning - values don’t change unexpectedly.
Basic Types Deep Dive
Atoms
Atoms are constants where name equals value:
:ok
:error
:hello
:user_not_found
:"hello world"Boolean values are atoms:
true == :true # => true
false == :false # => true
nil == :nil # => trueCommon in pattern matching:
status = :ok
case status do
:ok -> "Success"
:error -> "Failed"
endTuples
Fixed-size collections with contiguous memory:
tuple = {:ok, "message", 42}
elem(tuple, 0) # => :ok
elem(tuple, 1) # => "message"
put_elem(tuple, 1, "updated") # => {:ok, "updated", 42}
tuple # => {:ok, "message", 42} - original unchanged
tuple_size(tuple) # => 3Use tuples for fixed-size, heterogeneous data.
Lists
Linked lists - efficient head operations:
list = [1, 2, 3, 4, 5]
[0 | list] # => [0, 1, 2, 3, 4, 5]
list ++ [6] # => [1, 2, 3, 4, 5, 6]
[head | tail] = list
head # => 1
tail # => [2, 3, 4, 5]
length(list) # => 5Keyword Lists
Lists of 2-element tuples (key-value):
options = [name: "Alice", age: 30, city: "NYC"]
options == [{:name, "Alice"}, {:age, 30}, {:city, "NYC"}] # => true
options[:name] # => "Alice"
config = [port: 4000, port: 4001]
config[:port] # => 4000 (first value)Common for function options:
def connect(host, opts \\ []) do
port = opts[:port] || 80
timeout = opts[:timeout] || 5000
# ...
end
connect("localhost", port: 4000, timeout: 3000)Maps
Key-value stores with unique keys:
user = %{name: "Bob", age: 25, email: "bob@example.com"}
user[:name] # => "Bob"
user.name # => "Bob" (atom keys only)
updated = %{user | age: 26}
user.age # => 25 - original unchanged
updated.age # => 26
with_city = Map.put(user, :city, "Boston")
mixed = %{"string" => 1, :atom => 2, 42 => 3}Use maps for key-value data and structs.
Structs
Maps with defined schema and defaults:
defmodule User do
defstruct name: "", age: 0, email: nil
end
user = %User{name: "Alice", age: 30}
user.name # => "Alice"
user.age # => 30
user.email # => nil (default)
%User{name: name} = user
name # => "Alice"
updated = %{user | age: 31}Structs are just maps with __struct__ key:
user.__struct__ # => UserFunctions and Modules
Anonymous Functions
Functions as first-class values:
greet = fn name -> "Hello, #{name}!" end
greet.("Alice") # => "Hello, Alice!"
add = fn a, b -> a + b end
add.(2, 3) # => 5
upcase = &String.upcase/1
upcase.("hello") # => "HELLO"Named Functions in Modules
defmodule Math do
# Public function
def add(a, b) do
a + b
end
# Private function
defp multiply(a, b) do
a * b
end
# One-liner syntax
def subtract(a, b), do: a - b
# Multiple clauses (pattern matching!)
def factorial(0), do: 1
def factorial(n) when n > 0, do: n * factorial(n - 1)
end
Math.add(2, 3) # => 5
Math.subtract(10, 3) # => 7
Math.factorial(5) # => 120Function Clauses
Functions can have multiple definitions - Elixir matches top-to-bottom:
defmodule Validator do
# Match empty string
def validate(""), do: {:error, "Empty string"}
# Match nil
def validate(nil), do: {:error, "Nil value"}
# Guard clause
def validate(str) when is_binary(str) and byte_size(str) > 100 do
{:error, "Too long"}
end
# Default case
def validate(str) when is_binary(str), do: {:ok, str}
# Non-string
def validate(_other), do: {:error, "Not a string"}
end
Validator.validate("") # => {:error, "Empty string"}
Validator.validate(nil) # => {:error, "Nil value"}
Validator.validate("hi") # => {:ok, "hi"}
Validator.validate(String.duplicate("x", 101)) # => {:error, "Too long"}
Validator.validate(123) # => {:error, "Not a string"}Default Arguments
defmodule Greeter do
def greet(name, greeting \\ "Hello") do
"#{greeting}, #{name}!"
end
end
Greeter.greet("Alice") # => "Hello, Alice!"
Greeter.greet("Bob", "Howdy") # => "Howdy, Bob!"Pipeline Operator
The |> operator threads result of one function as first argument to next:
Basic Pipeline
String.upcase(String.trim(" hello "))
" hello "
|> String.trim()
|> String.upcase()Each line passes result to next function’s first argument.
Complex Pipeline Example
defmodule TextProcessor do
def process(text) do
text
|> String.trim()
|> String.downcase()
|> String.split()
|> Enum.map(&String.capitalize/1)
|> Enum.join(" ")
end
end
TextProcessor.process(" HELLO WORLD ")Step-by-step:
" HELLO WORLD " |> String.trim()→"HELLO WORLD""HELLO WORLD" |> String.downcase()→"hello world""hello world" |> String.split()→["hello", "world"]["hello", "world"] |> Enum.map(&String.capitalize/1)→["Hello", "World"]["Hello", "World"] |> Enum.join(" ")→"Hello World"
Pipeline with Multiple Arguments
Pipeline passes value as first argument:
"hello"
|> String.duplicate(3)
[1, 2, 3, 4]
|> Enum.map(fn x -> x * 2 end)
|> Enum.filter(fn x -> x > 4 end)
|> Enum.sum()Recursion - Functional Iteration
Elixir has no loops - use recursion instead.
Basic Recursion
defmodule Counter do
def count_down(0), do: IO.puts("Done!")
def count_down(n) when n > 0 do
IO.puts(n)
count_down(n - 1)
end
end
Counter.count_down(3)Output:
3
2
1
Done!Tail Recursion
Tail-recursive functions (recursive call is last operation) are optimized:
defmodule Sum do
# Non-tail recursive (NOT optimized)
def sum([]), do: 0
def sum([head | tail]), do: head + sum(tail)
# Tail recursive (optimized!)
def sum_tail(list), do: sum_tail(list, 0)
defp sum_tail([], acc), do: acc
defp sum_tail([head | tail], acc) do
sum_tail(tail, acc + head)
end
end
Sum.sum([1, 2, 3, 4, 5]) # => 15
Sum.sum_tail([1, 2, 3, 4, 5]) # => 15 (uses constant stack space)Recursion with Accumulator
defmodule ListOps do
# Reverse list using accumulator
def reverse(list), do: reverse(list, [])
defp reverse([], acc), do: acc
defp reverse([head | tail], acc) do
reverse(tail, [head | acc])
end
end
ListOps.reverse([1, 2, 3, 4]) # => [4, 3, 2, 1]Enumerables - Working with Collections
Enum Module
Eager evaluation - processes immediately:
Enum.map([1, 2, 3], fn x -> x * 2 end)
Enum.filter([1, 2, 3, 4, 5], fn x -> rem(x, 2) == 0 end)
Enum.reduce([1, 2, 3, 4], 0, fn x, acc -> acc + x end)
Enum.any?([1, 2, 3], fn x -> x > 2 end) # => true
Enum.all?([1, 2, 3], fn x -> x > 0 end) # => true
Enum.find([1, 2, 3, 4], fn x -> x > 2 end) # => 3
Enum.take([1, 2, 3, 4, 5], 3) # => [1, 2, 3]
Enum.drop([1, 2, 3, 4, 5], 2) # => [3, 4, 5]
Enum.chunk_every([1, 2, 3, 4, 5, 6], 2)Stream Module
Lazy evaluation - processes on-demand:
stream = [1, 2, 3, 4, 5]
|> Stream.map(fn x -> IO.puts("Doubling #{x}"); x * 2 end)
|> Stream.filter(fn x -> IO.puts("Filtering #{x}"); x > 4 end)
result = Enum.to_list(stream)Output shows lazy evaluation:
Doubling 1
Filtering 2
Doubling 2
Filtering 4
Doubling 3
Filtering 6
Doubling 4
Filtering 8
Doubling 5
Filtering 10
[6, 8, 10]Each element processed completely before next element starts.
Infinite Streams
Stream.iterate(1, fn x -> x + 1 end)
|> Stream.map(fn x -> x * x end)
|> Enum.take(5)
Stream.cycle([1, 2, 3])
|> Enum.take(7)Comprehensions
Alternative syntax for transformations:
for x <- [1, 2, 3, 4], do: x * 2
for x <- [1, 2, 3, 4], x > 2, do: x * 2
for x <- [1, 2], y <- [3, 4], do: {x, y}
for {k, v} <- %{a: 1, b: 2}, into: %{}, do: {k, v * 2}Processes - Lightweight Concurrency
Elixir processes are NOT OS processes - they’re lightweight VM processes.
Spawn Process
pid = spawn(fn ->
IO.puts("Hello from process #{inspect(self())}")
end)
IO.puts("Spawned process #{inspect(pid)}")Output:
Spawned process #PID<0.123.0>
Hello from process #PID<0.123.0>Process Identifiers
self() # => #PID<0.100.0>
pid = spawn(fn -> :timer.sleep(100) end)
Process.alive?(pid) # => true (immediately after spawn)
:timer.sleep(200)
Process.alive?(pid) # => false (after process exits)Linking Processes
spawn_link(fn ->
raise "Oops!"
end)
pid = spawn(fn -> :timer.sleep(100) end)
ref = Process.monitor(pid)
receive do
{:DOWN, ^ref, :process, ^pid, reason} ->
IO.puts("Process exited: #{inspect(reason)}")
endMessage Passing
Processes communicate via messages.
Send and Receive
send(self(), {:hello, "World"})
receive do
{:hello, msg} -> IO.puts("Received: #{msg}")
endPattern Matching in Receive
defmodule Receiver do
def loop do
receive do
{:ping, from} ->
send(from, :pong)
IO.puts("Sent pong")
loop()
{:print, message} ->
IO.puts(message)
loop()
:stop ->
IO.puts("Stopping")
:ok
_other ->
IO.puts("Unknown message")
loop()
end
end
end
pid = spawn(fn -> Receiver.loop() end)
send(pid, {:ping, self()})
send(pid, {:print, "Hello!"})
send(pid, :stop)
receive do
:pong -> IO.puts("Got pong!")
endTimeout in Receive
receive do
{:message, data} -> IO.puts(data)
after
1000 -> IO.puts("No message after 1 second")
endGenServer - OTP Behavior
GenServer (Generic Server) is OTP behavior for stateful server processes.
Basic GenServer
defmodule Counter do
use GenServer
## Client API
def start_link(initial_value) do
GenServer.start_link(__MODULE__, initial_value, name: __MODULE__)
end
def increment do
GenServer.cast(__MODULE__, :increment)
end
def get do
GenServer.call(__MODULE__, :get)
end
## Server Callbacks
@impl true
def init(initial_value) do
{:ok, initial_value}
end
@impl true
def handle_cast(:increment, state) do
{:noreply, state + 1}
end
@impl true
def handle_call(:get, _from, state) do
{:reply, state, state}
end
end
{:ok, _pid} = Counter.start_link(0)
Counter.increment()
Counter.increment()
Counter.increment()
Counter.get() # => 3Call vs Cast
GenServer.call - Synchronous, waits for reply:
def get_user(id) do
GenServer.call(__MODULE__, {:get_user, id})
end
def handle_call({:get_user, id}, _from, state) do
user = Map.get(state.users, id)
{:reply, user, state}
endGenServer.cast - Asynchronous, no reply:
def log(message) do
GenServer.cast(__MODULE__, {:log, message})
end
def handle_cast({:log, message}, state) do
IO.puts("[LOG] #{message}")
{:noreply, state}
endGenServer with Complex State
defmodule UserRegistry do
use GenServer
# Client API
def start_link(_opts) do
GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
end
def register(user_id, user_data) do
GenServer.call(__MODULE__, {:register, user_id, user_data})
end
def lookup(user_id) do
GenServer.call(__MODULE__, {:lookup, user_id})
end
def all_users do
GenServer.call(__MODULE__, :all_users)
end
# Server Callbacks
@impl true
def init(_) do
{:ok, %{}}
end
@impl true
def handle_call({:register, user_id, user_data}, _from, state) do
new_state = Map.put(state, user_id, user_data)
{:reply, :ok, new_state}
end
@impl true
def handle_call({:lookup, user_id}, _from, state) do
result = case Map.fetch(state, user_id) do
{:ok, user} -> {:ok, user}
:error -> {:error, :not_found}
end
{:reply, result, state}
end
@impl true
def handle_call(:all_users, _from, state) do
{:reply, Map.keys(state), state}
end
end
{:ok, _pid} = UserRegistry.start_link([])
UserRegistry.register(1, %{name: "Alice", age: 30})
UserRegistry.register(2, %{name: "Bob", age: 25})
UserRegistry.lookup(1) # => {:ok, %{name: "Alice", age: 30}}
UserRegistry.lookup(999) # => {:error, :not_found}
UserRegistry.all_users() # => [1, 2]Practical Example - URL Shortener
Let’s build a complete URL shortener combining concepts:
defmodule UrlShortener do
use GenServer
# Client API
def start_link(_opts) do
GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
end
def shorten(url) do
GenServer.call(__MODULE__, {:shorten, url})
end
def expand(short_code) do
GenServer.call(__MODULE__, {:expand, short_code})
end
def stats do
GenServer.call(__MODULE__, :stats)
end
# Server Callbacks
@impl true
def init(_) do
state = %{
urls: %{}, # short_code -> url
reverse: %{}, # url -> short_code
counter: 0
}
{:ok, state}
end
@impl true
def handle_call({:shorten, url}, _from, state) do
case Map.fetch(state.reverse, url) do
{:ok, existing_code} ->
# URL already shortened
{:reply, {:ok, existing_code}, state}
:error ->
# Generate new short code
short_code = generate_code(state.counter)
new_state = %{
urls: Map.put(state.urls, short_code, url),
reverse: Map.put(state.reverse, url, short_code),
counter: state.counter + 1
}
{:reply, {:ok, short_code}, new_state}
end
end
@impl true
def handle_call({:expand, short_code}, _from, state) do
result = case Map.fetch(state.urls, short_code) do
{:ok, url} -> {:ok, url}
:error -> {:error, :not_found}
end
{:reply, result, state}
end
@impl true
def handle_call(:stats, _from, state) do
stats = %{
total_urls: map_size(state.urls),
next_id: state.counter
}
{:reply, stats, state}
end
# Private Helpers
defp generate_code(counter) do
# Simple base62 encoding
counter
|> Integer.to_string(36)
|> String.downcase()
end
end
{:ok, _pid} = UrlShortener.start_link([])
{:ok, code1} = UrlShortener.shorten("https://example.com/very/long/url")
{:ok, code2} = UrlShortener.shorten("https://github.com/elixir-lang/elixir")
{:ok, code3} = UrlShortener.shorten("https://example.com/very/long/url") # Same URL
code1 # => "0"
code2 # => "1"
code3 # => "0" (returns existing code)
UrlShortener.expand("0") # => {:ok, "https://example.com/very/long/url"}
UrlShortener.expand("1") # => {:ok, "https://github.com/elixir-lang/elixir"}
UrlShortener.expand("999") # => {:error, :not_found}
UrlShortener.stats() # => %{total_urls: 2, next_id: 2}Running the Complete Example
Save this code to a file and run it:
iex url_shortener.exs
mix new url_shortener_appSummary
What you’ve learned:
- Pattern Matching - Destructuring and matching with
=, pin operator^, underscore_ - Immutability - Data transformation without mutation, benefits for concurrency
- Basic Types - Atoms, tuples, lists, keyword lists, maps, structs
- Functions - Anonymous functions, named functions, function clauses, default arguments
- Modules - Code organization, public/private functions
- Pipeline Operator - Composing functions with
|>for readable data flow - Recursion - Functional iteration, tail recursion optimization, accumulators
- Enumerables - Enum (eager) vs Stream (lazy), comprehensions
- Processes - Spawning, linking, monitoring lightweight processes
- Message Passing - Send/receive, pattern matching messages, timeouts
- GenServer - OTP behavior for stateful servers, call vs cast
Key skills gained:
- Writing functional, immutable code
- Composing operations with pipelines
- Working with Elixir collections effectively
- Building concurrent systems with processes
- Creating stateful servers with GenServer
- Pattern matching in multiple contexts
Core patterns mastered:
- Match operator for destructuring
- Function clauses for conditional logic
- Tail recursion for iteration
- Pipeline operator for composition
- GenServer for stateful processes
Next Steps
Ready for comprehensive learning?
Prefer code-first learning?
- By-Example Tutorial - 75-90 heavily annotated examples covering 95% of Elixir
Want to understand design philosophy?
- Overview - Why Elixir exists, when to use it, ecosystem overview
Ready to build web applications?
- Phoenix Framework Initial Setup - Install and configure Phoenix web framework
Practice Exercises
Try building these projects to solidify concepts:
1. Todo List GenServer
Build a GenServer that manages todo items:
- Add todo with description
- Mark todo as complete
- List all todos
- Delete todo
2. Temperature Converter Module
Create module with functions:
- Celsius to Fahrenheit
- Fahrenheit to Celsius
- Kelvin conversions
- Use pattern matching and guards
3. Concurrent File Processor
Spawn multiple processes to:
- Read multiple files concurrently
- Count words in each file
- Aggregate results
- Use message passing
4. Rate Limiter GenServer
Implement rate limiting:
- Track requests per time window
- Allow/deny based on limits
- Reset counters periodically
- Use GenServer state management
Further Resources
Official Documentation:
- Elixir Guides - Official comprehensive guides
- Standard Library - Complete API documentation
- Erlang/OTP - Underlying platform documentation
Books:
- Programming Elixir - Dave Thomas, comprehensive introduction
- Elixir in Action - Saša Jurić, practical focus
- Designing Elixir Systems with OTP - James Gray, OTP patterns
Interactive Learning:
- Exercism Elixir Track - Practice problems with mentorship
- Elixir School - Free lessons and examples
- Elixir Koans - Test-driven learning
Community:
- Elixir Forum - Official discussion forum
- Elixir Slack - Real-time community chat
- /r/elixir - Reddit community