Getting Started
Basics & IEx
Elixir runs on the Erlang VM (BEAM), known for concurrency and fault tolerance. Atoms are constants where the name is the value. <> concatenates strings. Tuples are fixed-size, lists are linked lists.
# IEx interactive shell
# run: iex
# simple values
iex> 1 + 2
3
iex> "hello" <> " world"
"hello world"
# atoms (constants where name is value)
iex> :ok
:ok
iex> :error
:error
# tuples
iex> {:ok, 42}
{:ok, 42}
# lists
iex> [1, 2, 3]
[1, 2, 3]Hello World
IO.puts prints with a trailing newline. #{expr} interpolates expressions into strings. .exs files are scripts (run directly); .ex files are compiled. IO.inspect is the debugging workhorse.
# hello.exs
IO.puts("Hello, World!")
# run: elixir hello.exs
# or in iex: iex hello.exs
# string interpolation with #{}:
name = "Elixir"
IO.puts("Hello, #{name}!")
# Hello, Elixir!
# IO.inspect shows any value (useful for debugging):
IO.inspect([1, 2, 3], label: "nums")Comments & Scripts
Elixir supports only single-line comments starting with #. Use .exs for scripts and .ex for compilable modules. In IEx, r Module reloads one module and recompile() rebuilds a Mix project.
# single line comment only
# Elixir has no multi-line comments
# script file (.exs) - executed directly
# elixir script.exs
# compiled module file (.ex) - compiled then run
# elixirc file.ex
# in IEx, reload a single module:
iex> r MyModule
# recompile the whole project:
iex> recompile()Help & Documentation
h shows docs, i introspects a value's type, s shows the function spec, b lists behaviour callbacks, t prints type definitions. These are essential tools for interactive learning and discovery.
# in IEx:
iex> h Enum.map # docs for Enum.map/2
iex> h Enum # module docs
iex> i "hello" # type info about a value
iex> b GenServer # list behaviour callbacks
iex> s Enum.map # function spec
iex> t Enum.t # print type definitions
# exit IEx:
iex> System.halt(0)
# or press Ctrl+C twiceProject Structure
Mix projects use lib/ for source, test/ for ExUnit tests, mix.exs for configuration. Module names map to file paths (MyApp.Greeter lives in lib/my_app/greeter.ex), which keeps things discoverable.
# Mix project layout:
my_app/
lib/ # source code (.ex)
my_app.ex
my_app/greeter.ex
test/ # ExUnit tests (.exs)
my_app_test.exs
mix.exs # project config & dependencies
config/ # config files
config.exs
# module names map to file paths:
# MyApp.Greeter -> lib/my_app/greeter.ex
defmodule MyApp.Greeter do
def hello(name), do: "Hi, #{name}"
endBasic Types
Atoms
Atoms are immutable constants whose name is their value. true, false, and nil are atoms. Don't create atoms from untrusted input—the atom table is not garbage-collected and can leak memory.
# atoms are constants where the name IS the value
:ok
:error
:success
:true # boolean true is an atom
:false # boolean false is an atom
nil # nil is also an atom
# create from a string:
String.to_atom("hello") # :hello
# atoms compared by name (alphabetical):
:apple < :banana # true
# existing? check:
Atom.to_string(:ok) # "ok"Numbers
Integers have arbitrary precision (no overflow). / always returns a float; use div/2 and rem/2 for integer operations. Supports hex (0x), octal (0o), and binary (0b) literals.
# integers (arbitrary precision)
42
-7
0x1F # hex = 31
0o17 # octal = 15
0b1010 # binary = 10
# floats
3.14
-0.5
1.0e3 # 1000.0
# division always returns a float:
7 / 2 # 3.5
div(7, 2) # 3 (integer division)
rem(7, 3) # 1 (remainder)
# rounding:
round(3.6) # 4
trunc(3.9) # 3Strings & Binaries
Double-quoted strings are binaries (UTF-8 bytes); single-quoted are charlists (rarely needed). Heredocs use triple double-quotes. Don't mix them—String functions expect binaries.
# double-quoted strings are UTF-8 binaries
"hello"
"Elixir" # UTF-8 supported
"#{1 + 1} cats" # "2 cats"
# single-quoted are charlists (list of codepoints)
'hello' == [104, 101, 108, 108, 111] # true
# heredoc for multiline strings:
"""
line 1
line 2
"""
is_binary("hi") # true
is_list('hi') # trueBooleans & nil
Only false and nil are falsy—everything else (including 0, [], "") is truthy. and/or/not require booleans; &&/||/! work on any value and return the operand itself.
true
false
nil
# only false and nil are falsy:
if nil, do: "yes", else: "no" # "no"
if 0, do: "yes", else: "no" # "yes" (0 is truthy!)
if [], do: "yes", else: "no" # "yes" (empty list is truthy)
# strict boolean ops (require booleans):
true and false # false
true or false # true
not true # false
# truthy ops (any value, return the value):
nil && 1 # nil
1 && 2 # 2
nil || "x" # "x"Lists & Tuples Overview
Lists are linked lists (fast prepend, slow random access). Tuples store fixed-size values contiguously (fast access by index). Keyword lists are lists of {atom, value} tuples; maps allow any keys.
# list (linked list)
[1, 2, 3]
[head | tail] = [1, 2, 3] # head=1, tail=[2,3]
[:a, "b", 3] # heterogeneous ok
# tuple (fixed-size, contiguous memory)
{:ok, 42}
{1, 2, 3}
tuple_size({:a, :b, :c}) # 3
# keyword list (list of 2-tuples with atom keys)
[name: "Alice", age: 30]
# map (any keys)
%{"name" => "Bob", :age => 25}Operators
Arithmetic Operators
/ always returns a float; use div/2 for integer division. rem/2 takes the sign of the dividend, Integer.mod/2 always non-negative. Integer.pow/2 (1.12+) does integer exponentiation.
1 + 2 # 3
5 - 3 # 2
2 * 4 # 8
10 / 2 # 5.0 (always float)
div(7, 2) # 3 (integer division)
rem(7, 3) # 1 (remainder, sign of dividend)
abs(-5) # 5
round(3.6) # 4
trunc(3.9) # 3
# power (Elixir 1.12+):
Integer.pow(2, 10) # 1024
:math.pow(2, 10) # 1024.0 (float)Comparison Operators
== compares values, === compares values and types. Elixir has a total ordering across all types (number < atom < ... < list < bitstring), so any two values can be compared without error.
1 == 1.0 # true (value equality, ignores type)
1 === 1.0 # false (strict, also checks type)
1 != 2 # true
1 !== 1.0 # true
2 > 1 # true
2 >= 2 # true
1 < 2 # true
1 <= 2 # true
# total ordering across types:
# number < atom < ref < fun < port < pid < tuple < map < list < bitstring
:atom > 1 # true
[1] > %{} # trueLogical Operators
and/or/not require booleans (raise otherwise) and short-circuit. &&/||/! accept any value, treating only false and nil as falsy, and return the operand itself rather than a boolean.
# strict (require boolean operands):
true and false # false
true or false # true
not true # false
# truthy (any operand, return the operand):
nil && 1 # nil
1 && 2 # 2
nil || "default" # "default"
false || 5 # 5
!nil # true
!1 # false
# short-circuit evaluation:
false and raise("never") # false (right side skipped)
true or raise("never") # true (right side skipped)String Concatenation
<> concatenates binaries (strings); ++ concatenates lists (including charlists). -- removes the first occurrence of each element on the right. Prefer interpolation over repeated <> for readability.
"hello" <> " " <> "world" # "hello world"
# interpolation (preferred):
name = "Alice"
"Hi, #{name}!" # "Hi, Alice!"
"Sum: #{1 + 2}" # "Sum: 3"
# charlist concatenation:
'abc' ++ 'de' # 'abcde'
# list concatenation and subtraction:
[1, 2] ++ [3, 4] # [1, 2, 3, 4]
[1, 2, 3] -- [2] # [1, 3] (removes first match)Match Operator
= matches the right side against the left pattern, binding variables. If values don't match, raises MatchError. Variables can be rebound (point to new values), but the underlying data is immutable.
# = is pattern matching, not assignment
x = 1
1 = x # ok (matches, since x is 1)
2 = x # raises MatchError
# destructuring:
{a, b} = {1, 2}
a # 1
b # 2
[left | rest] = [1, 2, 3] # left=1, rest=[2,3]
# variables can be rebound (data itself is immutable):
x = 1
x = 2 # rebinds x to 2Membership & Others
in/2 checks membership in lists, ranges, maps (keys), and binaries (substring). | prepends to a list and is also used in pattern matching for head/tail. |> is the pipe operator (covered in its own section).
# in/2 checks membership in an enumerable:
2 in [1, 2, 3] # true
:b in [:a, :b, :c] # true
"x" in "text" # true (substring in binary)
# range membership:
5 in 1..10 # true
# | prepends to a list:
[0 | [1, 2, 3]] # [0, 1, 2, 3]
# also used to split head/tail:
[head | tail] = [1, 2, 3]
# |> is the pipe operator (see Pipe section):
"hi" |> String.upcase() # "HI"Pattern Matching
Match Operator
= binds variables in the pattern to values from the right. Reusing a variable in one pattern forces equality between the matched values. _ matches (and discards) anything.
# = matches right against left
x = 42
{a, b, c} = {1, 2, 3}
[a, b, c] = [:x, :y, :z]
# reusing a variable in one pattern forces equality:
{n, n} = {1, 1} # ok, n = 1
{n, n} = {1, 2} # MatchError (1 != 2)
# ignore values with _:
{_, b, _} = {1, 2, 3} # b = 2
# bind multiple at once:
{_, {x, y}} = {:ok, {3, 4}} # x=3, y=4Pin Operator
^var pins a variable so it's matched against its current value instead of being rebound. Essential for matching against existing values in case clauses and function heads.
x = 1
# ^ pins a variable: match its current value, don't rebind
^x = 1 # ok (x already 1)
^x = 2 # MatchError
case {1, 2} do
{^x, second} -> "matched, second=#{second}"
_ -> "no match"
end
# "matched, second=2"
# in function heads:
def same_as_x?(^x), do: true
def same_as_x?(_), do: falseMatching Tuples
Tuples match by size and contents. Tagged tuples like {:ok, value} and {:error, reason} are idiomatic for return values. case lets you handle each shape with a separate clause.
{:ok, value} = {:ok, 42}
value # 42
{:error, reason} = {:error, :not_found}
reason # :not_found
# size must match:
{a, b} = {1, 2, 3} # MatchError (size 2 vs 3)
# nested matching:
{:user, {name, age}} = {:user, {"Alice", 30}}
name # "Alice"
# tagged tuples for results:
case File.read("missing.txt") do
{:ok, content} -> content
{:error, :enoent} -> "file not found"
endMatching Lists (head/tail)
[head | tail] decomposes a list into its first element and the rest. Lists match structurally—use this for recursive traversal. The empty list [] matches only the empty list.
# head and tail:
[head | tail] = [1, 2, 3]
head # 1
tail # [2, 3]
# fixed first elements + rest:
[a, b | rest] = [1, 2, 3, 4]
a # 1
b # 2
rest # [3, 4]
# empty list matches only []:
[] = [] # ok
[] = [1] # MatchError
# classic recursion pattern:
def sum([]), do: 0
def sum([head | tail]), do: head + sum(tail)Matching Maps
Map matching only checks the keys you name—a subset match succeeds even if extra keys exist. Use ^key => value to match a dynamic key. Structs match by name and fields.
# match on specific keys (subset is fine):
%{name: name} = %{name: "Alice", age: 30}
name # "Alice"
# match a specific value:
%{status: :active} = %{status: :active, id: 1} # ok
%{status: :closed} = %{status: :active} # MatchError
# variable as a key requires pinning:
key = :name
%{^key => value} = %{name: "Bob"}
value # "Bob"
# struct matching (also a map):
%User{name: n} = %User{name: "Carol", age: 40}Matching in Functions
Function clauses are tried in order; the first matching head wins. Pattern matching + guards replace if/else for dispatch. Include a catch-all clause (or FunctionClauseError fires on no match).
defmodule Geometry do
# clauses tried top to bottom; first match wins:
def area({:rectangle, w, h}), do: w * h
def area({:circle, r}), do: 3.14 * r * r
def area({:square, s}), do: s * s
def area(_), do: {:error, :unknown_shape}
end
Geometry.area({:rectangle, 3, 4}) # 12
Geometry.area({:circle, 2}) # 12.56
Geometry.area({:triangle, 3, 4}) # {:error, :unknown_shape}
# guards add conditions:
def classify(n) when n < 0, do: :negative
def classify(0), do: :zero
def classify(n) when n > 0, do: :positiveImmutability
Immutable Data
All Elixir data structures are immutable—functions return modified copies, never mutate the original. Variables are bindings to values; rebinding just points a variable elsewhere, leaving the old value untouched.
# data is never mutated; operations return new data
list = [1, 2, 3]
List.replace_at(list, 0, 99) # [99, 2, 3]
list # [1, 2, 3] (unchanged)
map = %{a: 1}
Map.put(map, :b, 2) # %{a: 1, b: 2}
map # %{a: 1} (unchanged)
# strings too:
s = "hello"
String.upcase(s) # "HELLO"
s # "hello"List Operations Return New Lists
Prepending with | is O(1) because lists are singly linked and share the tail. Appending with ++ is O(n) since it must walk the left list. The original list is shared, not copied—efficient and safe.
original = [1, 2, 3]
# prepend (fast, O(1)):
[0 | original] # [0, 1, 2, 3]
original # [1, 2, 3]
# append (slow, O(n)):
original ++ [4] # [1, 2, 3, 4]
original # [1, 2, 3]
# concatenation:
[1, 2] ++ [3, 4] # [1, 2, 3, 4]Map Updates
%{map | key: val} updates an existing key (raises if absent). Map.put_new/3 adds only if absent. These return new maps; the original is unchanged. Maps share structure internally for efficiency.
m = %{name: "Alice", age: 30}
# Map.put (add or update):
Map.put(m, :age, 31) # %{name: "Alice", age: 31}
# update syntax (key MUST exist):
%{m | age: 31} # %{name: "Alice", age: 31}
# %{m | height: 170} # KeyError (height not present)
# add only if missing:
Map.put_new(m, :role, "admin") # adds :role
Map.put_new(m, :age, 99) # unchanged (age exists)
# delete:
Map.delete(m, :age) # %{name: "Alice"}No Variable Mutation
Rebinding a variable makes it point to a new value but never mutates the old value. Other bindings to the old value (like y) keep seeing it. There is no in-place mutation API for any data structure.
# variables bind to values; rebinding is allowed but
# the bound VALUE never changes
x = [1, 2, 3]
y = x # y points to the same list
x = [4, 5] # x rebound, y still [1, 2, 3]
y # [1, 2, 3]
# there is NO in-place mutation API:
# list[0] = 99 -- not valid Elixir
# to "update", rebind the variable:
x = [99 | tl(x)] # x now points to a new listSharing & Efficiency
Persistent data structures share unmodified parts, so 'copies' are cheap (O(1) for list prepend, ~O(log n) for map updates). Immutability makes sharing safe across processes without locks or copying.
# immutability enables safe structural sharing:
base = [1, 2, 3, 4, 5]
shared = [0 | base] # [0, 1, 2, 3, 4, 5]
# `shared` reuses `base`'s nodes—no copy of the tail
# maps also share internally; updates are ~O(log n):
m = %{a: 1, b: 2, c: 3}
m2 = Map.put(m, :d, 4) # shares most of m
# because data is immutable, sharing is always safe—
# no defensive copies needed across processes.
pids = Enum.map(1..100, fn _ -> spawn(fn -> m end) end)Lists
Head and Tail
Lists are singly linked: head is the first element, tail is the rest (a list). hd/1 and tl/1 extract them (raising on []). Prepending is O(1); length/1 is O(n) because it walks the list.
[head | tail] = [1, 2, 3]
head # 1
tail # [2, 3]
hd([1, 2, 3]) # 1
tl([1, 2, 3]) # [2, 3]
# empty list has no head:
# hd([]) # raises ArgumentError
# prepend a first element:
[0 | [1, 2, 3]] # [0, 1, 2, 3]
# length is O(n):
length([1, 2, 3]) # 3Concatenation and Subtraction
++ concatenates (O(n) on the left list). -- removes the first occurrence of each right element. List.flatten/1 unwraps nesting. Prefer Enum functions over the List module for most operations.
[1, 2] ++ [3, 4] # [1, 2, 3, 4]
[] ++ [1] # [1]
# subtraction removes first occurrence of each right element:
[1, 2, 2, 3, 2] -- [2] # [1, 2, 3, 2] (only first 2)
[1, 2, 3] -- [4] # [1, 2, 3] (no-op)
# flatten nested lists:
List.flatten([1, [2, [3, 4]], 5]) # [1, 2, 3, 4, 5]
# fold (left):
List.foldl([1, 2, 3], 0, fn x, acc -> x + acc end) # 6List Comprehensions
for is a comprehension: generators (<-), filters, and a do expression. By default returns a list; into: changes the destination (map, string, etc.). Powerful for transforms and filtering in one expression.
# basic:
for n <- [1, 2, 3], do: n * 2 # [2, 4, 6]
# with filter:
for n <- 1..10, rem(n, 2) == 0, do: n # [2, 4, 6, 8, 10]
# multiple generators:
for x <- [:a, :b], y <- [1, 2], do: {x, y}
# [a: 1, a: 2, b: 1, b: 2]
# into: change the result container:
for n <- [1, 2, 3], into: %{}, do: {n, n * n}
# %{1 => 1, 2 => 4, 3 => 9}
# build a string:
for c <- ?a..?c, into: "", do: <<c>> # "abc"List Module Functions
The List module provides index-based and structural operations. List.first/last are O(n). List.wrap/1 normalizes input into a list (handy for option parsing). Use Enum for most general operations.
List.first([1, 2, 3]) # 1
List.last([1, 2, 3]) # 3
List.delete([1, 2, 3], 2) # [1, 3]
List.delete_at([1, 2, 3], 1) # [1, 3]
List.insert_at([1, 2, 3], 1, 9) # [1, 9, 2, 3]
List.replace_at([1, 2, 3], 1, 9) # [1, 9, 3]
# wrap (ensure list):
List.wrap(nil) # []
List.wrap(1) # [1]
List.wrap([1, 2]) # [1, 2]
# duplicate:
List.duplicate(:x, 3) # [:x, :x, :x]Enumerating Lists
Enum works on any enumerable (lists, maps, ranges). map/filter/reduce are the workhorses. chunk_every/2 splits into groups of size n; zip/2 pairs elements from two lists.
Enum.map([1, 2, 3], fn x -> x * 10 end) # [10, 20, 30]
Enum.filter([1, 2, 3, 4], fn x -> x > 2 end) # [3, 4]
Enum.reduce([1, 2, 3], 0, &+/2) # 6
Enum.find([1, 2, 3], fn x -> x > 1 end) # 2
# membership:
1 in [1, 2, 3] # true
Enum.member?([1, 2, 3], 2) # true
# chunking:
Enum.chunk_every([1, 2, 3, 4], 2) # [[1, 2], [3, 4]]
# zip:
Enum.zip([:a, :b], [1, 2]) # [{:a, 1}, {:b, 2}]Flattening and Folding
List.flatten/1 removes nesting. Enum.reduce/3 is the idiomatic left fold. List.foldr/3 folds from the right. Folding with prepend builds a reversed list efficiently in O(n).
# flatten:
List.flatten([1, [2, 3], [4, [5]]]) # [1, 2, 3, 4, 5]
# flatten with a tail:
List.flatten([1, [2]], [3, 4]) # [1, 2, 3, 4]
# fold (Enum.reduce is a left fold):
Enum.reduce([1, 2, 3], 0, fn x, acc -> acc + x end) # 6
# right fold:
List.foldr([1, 2, 3], [], fn x, acc -> [x | acc] end) # [1, 2, 3]
# building a reversed list with reduce + prepend:
Enum.reduce([1, 2, 3], [], fn x, acc -> [x | acc] end) # [3, 2, 1]Tuples
Creating Tuples
Tuples store a fixed number of items in contiguous memory (fast O(1) access by index). Use them for small, fixed-shape groupings—especially tagged tuples like {:ok, value}—not for growing collections.
{:ok, 42}
{1, 2, 3}
{}
{:point, 3, 4}
# from a list:
List.to_tuple([:a, :b, :c]) # {:a, :b, :c}
# duplicate elements:
Tuple.duplicate(:x, 3) # {:x, :x, :x}
# size (O(1)):
tuple_size({:a, :b, :c}) # 3Accessing Elements
elem/2 reads by 0-based index in O(1). put_elem/3 returns a new tuple (immutability). Pattern matching is more idiomatic than index access—it's clearer and self-documenting.
t = {:ok, "hello", 42}
elem(t, 0) # :ok
elem(t, 1) # "hello"
elem(t, 2) # 42
# tuples are 0-indexed; out of range raises:
# elem(t, 5) # ArgumentError
# put_elem returns a NEW tuple:
put_elem(t, 1, "world") # {:ok, "world", 42}
t # unchanged: {:ok, "hello", 42}
# pattern matching is preferred over index access:
{:ok, msg, _} = t
msg # "hello"Tagged Tuples (ok/error)
Tagged tuples ({:ok, value}, {:error, reason}) are Elixir's idiomatic way to signal success/failure without exceptions. case and with both destructure them. Always handle both branches.
# idiomatic return values:
def divide(_a, 0), do: {:error, :divide_by_zero}
def divide(a, b), do: {:ok, div(a, b)}
# handle with case:
case divide(10, 2) do
{:ok, result} -> "result: #{result}"
{:error, reason} -> "error: #{reason}"
end
# "result: 5"
# with chains matches across steps:
with {:ok, a} <- maybe_a(),
{:ok, b} <- maybe_b() do
a + b
endUpdating Tuples
Tuples are immutable; every 'update' returns a new tuple. Appending/inserting/deleting is O(n) because the whole tuple is copied. If you frequently add/remove elements, use a list or map instead.
t = {1, 2, 3}
# put_elem returns a new tuple:
put_elem(t, 0, 99) # {99, 2, 3}
# tuples are immutable:
t # {1, 2, 3}
# append/prepend/insert create a NEW tuple (O(n)):
Tuple.append(t, 4) # {1, 2, 3, 4}
Tuple.insert_at(t, 1, 99) # {1, 99, 2, 3}
# delete:
Tuple.delete_at(t, 1) # {1, 3}Tuple Module
The Tuple module provides structural operations (append, insert_at, delete_at, duplicate). Tuple.to_list/1 and List.to_tuple/1 convert between the two. tuple_size/1 is O(1).
t = {:a, :b, :c}
Tuple.append(t, :d) # {:a, :b, :c, :d}
Tuple.delete_at(t, 0) # {:b, :c}
Tuple.duplicate(:x, 3) # {:x, :x, :x}
Tuple.insert_at(t, 1, :z) # {:a, :z, :b, :c}
# conversions:
Tuple.to_list(t) # [:a, :b, :c]
List.to_tuple([1, 2, 3]) # {1, 2, 3}
# size:
tuple_size(t) # 3Keyword Lists
Creating Keyword Lists
Keyword lists are lists of {atom, value} tuples with a special literal syntax. Keys must be atoms and duplicates are allowed. They're just lists, so all list operations apply (and access is O(n)).
# literal syntax (list of 2-tuples with atom keys):
[name: "Alice", age: 30]
# equivalent to:
[{:name, "Alice"}, {:age, 30}]
# duplicate keys allowed:
[a: 1, a: 2] # [a: 1, a: 2]
# empty:
[]
# a keyword list is just a list:
is_list([a: 1]) # true
length([a: 1, b: 2]) # 2Accessing Values
Access with kw[:key] returns the first matching value (nil if absent). Keyword.get_values/2 returns all values for a duplicated key. Keyword lists are O(n) per lookup—use maps for frequent access.
kw = [name: "Alice", age: 30, role: :admin]
# access first value for a key:
kw[:name] # "Alice"
kw[:age] # 30
kw[:missing] # nil
# Keyword.get with a default:
Keyword.get(kw, :missing, "default") # "default"
# get ALL values for a key (when duplicates exist):
Keyword.get_values([a: 1, a: 2], :a) # [1, 2]
# strict fetch (raises if missing):
Keyword.fetch!(kw, :name) # "Alice"Keyword Module
The Keyword module mirrors many Map functions but operates on lists of tuples. put/3 prepends a new pair (does not replace existing—use delete then put to replace). Operations are O(n).
kw = [a: 1, b: 2, c: 3]
Keyword.keys(kw) # [:a, :b, :c]
Keyword.values(kw) # [1, 2, 3]
Keyword.has_key?(kw, :a) # true
Keyword.put(kw, :d, 4) # [a: 1, b: 2, c: 3, d: 4]
Keyword.delete(kw, :b) # [a: 1, c: 3]
# merge (right side wins on conflicts):
Keyword.merge([a: 1], [a: 99, b: 2]) # [a: 99, b: 2]
# take/drop:
Keyword.take(kw, [:a, :c]) # [a: 1, c: 3]Keyword vs Maps
Keyword lists are for ordered, atom-keyed option lists (like function options). Maps are for general key/value storage with any key type and fast lookup. Don't reach for keyword lists as a primary data structure.
# Keyword list: ordered, atom keys only, duplicates, O(n)
[name: "Alice", age: 30]
# Map: unordered, any keys, no duplicates, ~O(log n)
%{name: "Alice", age: 30}
# use keyword lists for function options (idiomatic):
String.split("a,b,c", ",", trim: true)
# use keyword lists for small ordered config:
Application.get_env(:my_app, :key, [])
# use maps for:
# - key/value data with mixed-type or string keys
# - frequent lookupsCommon Use Cases
Keyword lists shine as the last argument of functions for options (e.g., trim: true). The do/end block syntax is actually a keyword list in disguise: if(true, do: x, else: y). Config files use them heavily.
# function options (most common):
def greet(name, opts \\ []) do
prefix = Keyword.get(opts, :prefix, "Hello")
"#{prefix}, #{name}!"
end
greet("Alice", prefix: "Hi") # "Hi, Alice!"
# config files:
config :my_app, MyRepo,
pool_size: 10,
timeout: 5000
# do/end block sugar is a keyword list:
if true, do: :yes, else: :no
# is shorthand for:
if(true, do: :yes, else: :no)Maps
Creating Maps
Maps hold key/value pairs with any key type. The %{key: val} syntax requires atom keys; use key => val for other types. map_size/1 is O(1). Maps are the primary key/value data structure.
# atom keys (shorthand):
%{name: "Alice", age: 30}
# any keys (=>):
%{"name" => "Bob", 1 => :one, :ok => true}
# mixed keys:
%{:atom => 1, "string" => 2}
# from a keyword list:
Enum.into([a: 1, b: 2], %{}) # %{a: 1, b: 2}
# empty:
%{}
# size (O(1)):
map_size(%{a: 1, b: 2}) # 2Accessing Values
m[:key] returns nil for missing keys (safe). m.key (dot) is atom-key-only and raises KeyError if absent—use it when the key is required. Map.fetch/2 returns {:ok, val} or :error for explicit handling.
m = %{name: "Alice", age: 30, role: :admin}
# bracket access (nil if missing):
m[:name] # "Alice"
m[:missing] # nil
# dot access (atom keys only, raises if missing):
m.name # "Alice"
# m.missing # KeyError
# strict fetch:
Map.fetch!(m, :age) # 30
Map.fetch(m, :missing) # :error
# with a default:
Map.get(m, :missing, "default") # "default"Updating Maps
%{map | key: val} updates an existing key (KeyError if absent). Map.put/3 adds or updates. Map.merge/2 combines (right side wins on conflicts). All return new maps; the original is unchanged.
m = %{name: "Alice", age: 30}
# update existing key (raises if missing):
%{m | age: 31} # %{name: "Alice", age: 31}
# add or update:
Map.put(m, :age, 31) # %{name: "Alice", age: 31}
Map.put(m, :role, :admin) # adds :role
# add only if missing:
Map.put_new(m, :role, :admin)
# delete:
Map.delete(m, :age) # %{name: "Alice"}
# merge (right side wins on conflicts):
Map.merge(%{a: 1}, %{a: 99, b: 2}) # %{a: 99, b: 2}Map Module Functions
Map.keys/values return lists (order unspecified). take/drop select or remove keys. put_in/get_in/update_in navigate and modify nested structures ergonomically.
m = %{a: 1, b: 2, c: 3}
Map.keys(m) # [:a, :b, :c] (order not guaranteed)
Map.values(m) # [1, 2, 3]
Map.has_key?(m, :a) # true
Map.take(m, [:a, :c]) # %{a: 1, c: 3}
Map.drop(m, [:b]) # %{a: 1, c: 3}
# transform:
Map.new([{:a, 1}, {:b: 2}]) # %{a: 1, b: 2}
Map.to_list(m) # [a: 1, b: 2, c: 3]
# nested update:
users = %{alice: %{age: 30}}
put_in(users, [:alice, :age], 31) # %{alice: %{age: 31}}Pattern Matching Maps
Map patterns match on a subset of keys—extra keys are fine. To match a variable key, pin it with ^. This makes maps excellent for destructuring API responses and configuration.
# subset matching (extra keys ignored):
%{name: name} = %{name: "Alice", age: 30}
name # "Alice"
# match a specific value:
%{status: :active} = %{status: :active, id: 1} # ok
# variable key (must be pinned):
key = :name
%{^key => value} = %{name: "Bob"}
value # "Bob"
# in case clauses:
case response do
%{status_code: 200, body: body} -> body
%{status_code: code} -> {:error, code}
endStructs
Structs are tagged maps with a fixed set of keys and defaults, defined with defstruct. They carry their module name (__struct__) and enforce their keys at compile time—use them for domain entities.
defmodule User do
defstruct name: "anon", age: 0, role: :user
end
# create:
%User{name: "Alice", age: 30}
# %User{age: 30, name: "Alice", role: :user}
# access:
u = %User{name: "Bob"}
u.name # "Bob"
u.__struct__ # User
# update (existing fields only):
%{u | age: 25} # %User{age: 25, name: "Bob", role: :user}
# unknown key raises at compile time:
# %User{height: 180} # KeyError / compile error
# pattern match:
%User{name: n} = u
n # "Bob"Enum
map and each
map/2 transforms each element into a new list (same length). each/2 is for side effects and returns :ok. Enum works on any enumerable: lists, maps (as {k,v} pairs), ranges, etc.
Enum.map([1, 2, 3], fn x -> x * 2 end) # [2, 4, 6]
Enum.map([1, 2, 3], &(&1 * 2)) # [2, 4, 6] (capture)
# each returns :ok (side effects only):
Enum.each([1, 2, 3], fn x -> IO.puts(x) end)
# prints 1, 2, 3; returns :ok
# with index:
Enum.with_index([:a, :b, :c]) # [{:a, 0}, {:b, 1}, {:c, 2}]
# over a map (yields {key, value} pairs):
Enum.map(%{a: 1, b: 2}, fn {k, v} -> {k, v * 10} end)
# [a: 10, b: 20]filter and reject
filter keeps elements where the function returns truthy; reject is the inverse. For filter+map in one pass, use a for comprehension or Enum.flat_map with conditional lists.
Enum.filter([1, 2, 3, 4, 5], fn x -> rem(x, 2) == 0 end) # [2, 4]
Enum.reject([1, 2, 3, 4, 5], fn x -> rem(x, 2) == 0 end) # [1, 3, 5]
# truthy filter (drop nil/false):
Enum.filter([nil, 1, false, 2], & &1) # [1, 2]
# filter + map in one pass via flat_map:
Enum.flat_map([1, 2, 3], fn x ->
if x > 1, do: [x * 10], else: []
end)
# [20, 30]reduce
reduce/3 folds an enumerable into a single accumulator. Pass an explicit initial value to handle empty lists. reduce/2 without an initial value uses the first element (raises on []).
# sum:
Enum.reduce([1, 2, 3, 4], 0, fn x, acc -> acc + x end) # 10
# shorthand with a captured operator:
Enum.reduce([1, 2, 3, 4], 0, &+/2) # 10
# build a map:
Enum.reduce([{:a, 1}, {:b, 2}], %{}, fn {k, v}, acc ->
Map.put(acc, k, v * 2)
end)
# %{a: 2, b: 4}
# without an initial acc: uses the first element (raises on empty):
Enum.reduce([1, 2, 3], fn x, acc -> x + acc end) # 6find and find_value
find returns the first matching element; find_value returns the function's truthy result (handy for transforms with a sentinel). any?/all? short-circuit on the answer without scanning the whole list.
Enum.find([1, 2, 3, 4], fn x -> x > 2 end) # 3 (first match)
Enum.find([1, 2, 3], fn x -> x > 5 end) # nil (no match)
# find_value returns the FUNCTION result, not the element:
Enum.find_value([1, 2, 3], fn x -> x > 2 && x * 10 end) # 30
# with a default:
Enum.find([1, 2, 3], :none, fn x -> x > 5 end) # :none
# find_index:
Enum.find_index([:a, :b, :c], &(&1 == :b)) # 1
# existence checks (short-circuit):
Enum.any?([1, 2, 3], &(&1 > 2)) # true
Enum.all?([1, 2, 3], &(&1 > 0)) # truesort and sort_by
sort/1 ascending; sort/2 with :desc or a comparator. sort_by/2 extracts a key per element and sorts—cleaner than a custom comparator for structured data. min/max raise on empty lists.
Enum.sort([3, 1, 2]) # [1, 2, 3]
Enum.sort([3, 1, 2], :desc) # [3, 2, 1]
# custom comparator:
Enum.sort(["aa", "b", "ccc"], fn a, b -> String.length(a) <= String.length(b) end)
# ["b", "aa", "ccc"]
# sort_by (extract a key per element, sort by it):
Enum.sort_by([%{n: 3}, %{n: 1}, %{n: 2}], & &1.n)
# [%{n: 1}, %{n: 2}, %{n: 3}]
# min / max / min_max:
Enum.min([3, 1, 2]) # 1
Enum.max([3, 1, 2]) # 3
Enum.min_max([3, 1, 2]) # {1, 3}group_by and count
group_by/2 buckets elements by a key function into a map. count/1 counts all, count/2 counts matches. chunk_every splits by size, chunk_by by a changing key. uniq/1 removes duplicates (preserves order).
# group_by buckets elements by a key function:
Enum.group_by([1, 2, 3, 4, 5, 6], fn x -> rem(x, 3) end)
# %{0 => [3, 6], 1 => [1, 4], 2 => [2, 5]}
# group structs by a field:
Enum.group_by([%{t: :a}, %{t: :b}, %{t: :a}], & &1.t)
# count:
Enum.count([1, 2, 3]) # 3
Enum.count([1, 2, 3, 4], fn x -> x > 2 end) # 2
# chunk (split into groups):
Enum.chunk_every([1, 2, 3, 4, 5], 2) # [[1, 2], [3, 4], [5]]
Enum.chunk_by([1, 1, 2, 2, 3], & &1) # [[1, 1], [2, 2], [3]]
# uniq:
Enum.uniq([1, 1, 2, 3, 3]) # [1, 2, 3]Streams
Lazy Sequences
Streams are lazy enumerables: map/filter/etc. compose a pipeline that runs only when consumed (e.g., by Enum). This avoids intermediate lists—useful for large or infinite sequences.
# Stream is lazy—operations compose, nothing runs until consumed:
stream = Stream.map([1, 2, 3], fn x -> x * 2 end)
# #Stream<[enum: 1..3, funs: [...]]> (no computation yet)
# consume to force evaluation:
Enum.to_list(stream) # [2, 4, 6]
Enum.take(stream, 2) # [2, 4]
# pipeline of lazy ops:
[1, 2, 3]
|> Stream.map(&(&1 * 2))
|> Stream.filter(&(&1 > 2))
|> Enum.to_list() # [4, 6]Infinite Streams
Streams can be infinite because they're lazy—take/2 forces only the needed elements. iterate, repeatedly, and unfold all generate infinite streams. unfold is the most general (it carries state between elements).
# Stream.iterate(start, next) - infinite:
Stream.iterate(1, &(&1 + 1))
|> Enum.take(5) # [1, 2, 3, 4, 5]
# Stream.repeatedly(fun) - infinite calls:
Stream.repeatedly(fn -> :rand.uniform(10) end)
|> Enum.take(3) # e.g. [4, 7, 2]
# Stream.unfold(state, fun) - stateful infinite:
Stream.unfold(0, fn n -> {n, n + 1} end)
|> Enum.take(4) # [0, 1, 2, 3]
# fibonacci:
Stream.unfold({0, 1}, fn {a, b} -> {a, {b, a + b}} end)
|> Enum.take(8) # [0, 1, 1, 2, 3, 5, 8, 13]Cycling Streams
Stream.cycle/1 repeats an enumerable indefinitely. Great for zipping a finite list with a repeating pattern. Forgetting to take/2 on an infinite stream causes an infinite loop.
# Stream.cycle repeats an enumerable forever:
Stream.cycle([:a, :b, :c])
|> Enum.take(7) # [:a, :b, :c, :a, :b, :c, :a]
# alternate a pattern:
Stream.cycle([true, false])
|> Enum.take(5) # [true, false, true, false, true]
# zip a finite list with a repeating pattern:
Enum.zip([1, 2, 3, 4, 5], Stream.cycle([:x, :y]))
# [{1, :x}, {2, :y}, {3, :x}, {4, :y}, {5, :x}]Resource Streams
Stream.resource/3 wraps external resources (files, sockets, DB cursors) in a lazy stream: open once, yield elements on demand, close when consumed or halted. Great for memory-efficient file processing.
# Stream.resource(open, next, close) manages external resources:
stream = Stream.resource(
fn -> File.open!("data.txt") end, # open once
fn file ->
case IO.read(file, :line) do
:eof -> {:halt, file}
line -> {[line], file}
end
end,
fn file -> File.close(file) end # close always
)
# consume line by line - the file closes when done:
stream |> Enum.take(3)Stream vs Enum
Enum is eager and creates intermediate collections; Stream is lazy and fuses operations into a single pass. Use Stream for large datasets or pipelines that short-circuit (take, find). For small data, Enum is simpler and often faster.
# Enum: eager, returns a new collection, runs immediately:
1..1_000_000 |> Enum.map(&(&1 * 2)) |> Enum.filter(&(&1 > 2))
# builds two large intermediate lists
# Stream: lazy, composes, runs once when consumed:
1..1_000_000
|> Stream.map(&(&1 * 2))
|> Stream.filter(&(&1 > 2))
|> Enum.take(5) # only computes until 5 are found
# rule of thumb:
# - small/finite data, need it now -> Enum
# - huge/infinite data, or early exit -> StreamFunctions
Anonymous Functions
Anonymous functions use fn ... end and are called with a dot: fun.(args). &(&1 * &1) is the capture shorthand (&1, &2 are positional args). Capture named functions with &Mod.fun/arity. They support multi-clause pattern matching.
# define with fn ... end:
add = fn a, b -> a + b end
add.(1, 2) # 3
# shorthand capture:
square = &(&1 * &1)
square.(5) # 25
# capture a named function:
upcase = &String.upcase/1
upcase.("hi") # "HI"
# multi-clause anonymous functions:
greet = fn
:morning -> "Good morning"
:evening -> "Good evening"
_ -> "Hello"
end
greet.(:morning) # "Good morning"Named Functions
def defines public, defp private named functions. Multiple clauses with guards (when) dispatch by pattern. Without a matching clause, raises FunctionClauseError. The do: one-liner syntax is common for short bodies.
defmodule Math do
# public:
def square(x), do: x * x
# private (only callable within the module):
defp secret, do: 42
# multi-clause with guards:
def sign(n) when n > 0, do: 1
def sign(n) when n < 0, do: -1
def sign(0), do: 0
end
Math.square(4) # 16
Math.sign(-3) # -1
# Math.secret() # UndefinedFunctionError (private)Function Capture
&Mod.fun/arity captures a named function as an anonymous one. &(&1 ...) builds a lambda via the capture shorthand. Operators like +/2 are functions and can be captured or passed to reduce.
# capture a named function by module/name/arity:
upcase = &String.upcase/1
upcase.("hi") # "HI"
# capture a local function:
defmodule M do
def double(x), do: x * 2
def quad(x), do: double(x) |> double()
end
# partial application with the capture shorthand:
rem_10 = &rem(&1, 10)
rem_10.(23) # 3
# operators are functions too:
add = &+/2
add.(3, 4) # 7Default Arguments
\\ sets a default value for an argument, evaluated each call. Defaults generate multiple clause heads internally, which can interact subtly with explicit multi-clause functions—define defaults only in a header clause.
defmodule Greet do
# \\ sets a default value:
def hello(name, greeting \\ "Hello", punctuation \\ "!") do
"#{greeting}, #{name}#{punctuation}"
end
end
Greet.hello("Alice") # "Hello, Alice!"
Greet.hello("Alice", "Hi") # "Hi, Alice!"
Greet.hello("Alice", "Hey", "?") # "Hey, Alice?"
# defaults interact with multi-clause functions:
# define defaults only in a header clause (no body),
# then write explicit clauses for each arity.Multi-clause Functions
A named function can have many clauses (each with its own pattern/guard); the first match wins. Order clauses from most to least specific. The last clause is usually a catch-all to avoid FunctionClauseError.
defmodule FizzBuzz do
def fb(n) when rem(n, 15) == 0, do: "FizzBuzz"
def fb(n) when rem(n, 3) == 0, do: "Fizz"
def fb(n) when rem(n, 5) == 0, do: "Buzz"
def fb(n), do: to_string(n)
end
Enum.map(1..5, &FizzBuzz.fb/1)
# ["1", "2", "Fizz", "4", "Buzz"]
# clause order matters - first match wins, so put
# specific clauses before the general catch-all.Closures
Anonymous functions close over variables in scope, capturing them by value at definition time. Rebinding the variable afterward doesn't affect the closure. To share mutable state, use a process (see the Processes section).
# anonymous functions capture variables from their defining scope:
x = 10
f = fn -> x end
f.() # 10
# captured by VALUE at definition time:
x = 99
f.() # still 10 (the old binding)
# closures cannot share mutable state (data is immutable);
# to share state, use a process:
defmodule Counter do
def start, do: spawn(fn -> loop(0) end)
defp loop(n) do
receive do
{:inc, pid} -> send(pid, {:count, n + 1}); loop(n + 1)
end
end
endPipe Operator
Basic Pipe
|> takes the value on its left and feeds it as the first argument to the function call on its right. It makes data transformation read top-to-bottom, left-to-right, mirroring how you think about pipelines.
# |> passes the left value as the FIRST argument to the right call:
"hello" |> String.upcase() # "HELLO"
# equivalent to: String.upcase("hello")
[1, 2, 3] |> Enum.map(&(&1 * 2)) # [2, 4, 6]
# equivalent to: Enum.map([1, 2, 3], &(&1 * 2))
5 |> Integer.to_string() # "5"
:ok |> inspect() # ":ok"Multi-step Pipelines
Chaining |> reads naturally: each step transforms the previous result. The nested-call version reads inside-out and is much harder to follow. Pipelines are the idiomatic Elixir style for multi-step transforms.
"1,2,3,4,5"
|> String.split(",")
|> Enum.map(&String.to_integer/1)
|> Enum.filter(&(&1 > 2))
|> Enum.sum()
# 12 (3 + 4 + 5)
# without pipes (harder to read, nested inside-out):
Enum.sum(
Enum.filter(
Enum.map(String.split("1,2,3,4,5", ","), &String.to_integer/1),
&(&1 > 2)
)
)Working with Tuples
Pipe works on any value, including tuples. When a step can fail, use case to branch. tap/2 (Kernel) runs a side-effect function and returns the original value—handy for logging mid-pipeline.
# pipe into functions returning tuples, then destructure:
{:ok, content} =
"config.json"
|> File.read!()
|> Jason.decode!()
# use case when a step might fail:
"file.txt"
|> File.read()
|> case do
{:ok, body} -> body
{:error, _} -> "default"
end
# tap/2 runs a side effect without breaking the pipe:
[1, 2, 3]
|> Enum.map(&(&1 * 2))
|> tap(&IO.inspect(&1, label: "doubled"))
|> Enum.sum()Pipe with Anonymous Functions
An anonymous function in a pipe needs the explicit .() call, which looks awkward—prefer named functions or captures. then/2 (1.12+) applies a function to the piped value, useful for inline transforms.
# an anonymous function in a pipe must be called with a dot:
[1, 2, 3]
|> (fn list -> Enum.reverse(list) end).()
# [3, 2, 1]
# cleaner: use a named function or capture:
[1, 2, 3] |> Enum.reverse() # [3, 2, 1]
# then/2 (Elixir 1.12+) applies a function to the piped value:
5 |> then(fn x -> x * x end) # 25
# useful for inline construction:
def greet(name) do
name |> then(&"Hi, #{&1}!")
endBest Practices
Design your own functions data-first (the value as the first argument) so they pipe well. Keep pipelines to a readable length; extract named functions for reuse and tests. Use tap/2 for side effects, not arbitrary prints.
# DO: one operation per step, readable top-to-bottom:
users
|> Enum.filter(& &1.active)
|> Enum.map(& &1.name)
|> Enum.sort()
# DO: extract long pipelines into named functions for reuse/tests:
def active_names(users) do
users |> Enum.filter(& &1.active) |> Enum.map(& &1.name)
end
# AVOID: more than ~5 steps in one pipe - split or name intermediates.
# AVOID: mixing side effects into a transform pipeline - use tap/2.
# AVOID: piping into functions whose first arg isn't the data -
# restructure with a small wrapper.
# design your own functions data-first so they pipe well.Modules
defmodule
defmodule groups named functions. Module names are atoms (Greeter == :"Elixir.Greeter"). Nested modules form hierarchies (MyApp.Net.HTTP). @moduledoc documents the module; show in IEx with h Greeter.
defmodule Greeter do
@moduledoc "A simple greeting module."
def hello(name), do: "Hello, #{name}!"
def goodbye(name), do: "Bye, #{name}!"
end
Greeter.hello("Alice") # "Hello, Alice!"
# nested modules form a hierarchy:
defmodule MyApp.Net.HTTP do
def get(url), do: "GET #{url}"
end
MyApp.Net.HTTP.get("/users")def and defp
def defines public (exported) functions; defp defines private helpers usable only within the module. Both support the do: one-liner and do/end block forms. Private functions aid encapsulation and are inlined by the compiler.
defmodule Account do
# public API:
def balance(user), do: user.balance
# private helper (not exported):
defp normalize(name), do: String.downcase(name)
# one-liner and do/end forms:
def create(name) do
%{name: normalize(name), balance: 0}
end
end
Account.balance(%{balance: 100}) # 100
# Account.normalize("ALICE") # undefined (private)import
import brings another module's functions/macros into local scope so you can call them without the prefix. Use only/except to avoid polluting the namespace. Prefer import over fully-qualified names for frequently-used helpers.
# bring a module's functions into local scope (no prefix):
defmodule Colors do
import String
def shout(s), do: upcase(s) <> "!" # calls String.upcase
end
# only/except to narrow what's imported:
import Enum, only: [map: 2, filter: 2]
import String, except: [split: 2]
# import only functions or only macros:
import SomeModule, only: :functions
import SomeMacroModule, only: :macrosalias
alias gives a module a shorter local name (default: last segment). Use :as for custom names, and the multi-alias form alias MyApp.{A, B} to group. Aliases are lexical—only visible in the module that declares them.
# alias creates a short name for a module:
defmodule MyModule do
alias MyApp.Very.Long.Path.HTTPClient
def fetch(url) do
HTTPClient.get(url) # instead of MyApp.Very.Long.Path.HTTPClient.get
end
end
# custom alias name with :as:
alias MyApp.Net.HTTPClient, as: HTTP
# multiple aliases at once:
alias MyApp.{Repo, Schema.User}require
require is needed to invoke a module's macros (which are expanded at compile time). Functions don't need require. Logger.info/1 is a macro, so modules using Logger must require Logger first.
# require lets you use a module's MACROS (not needed for functions):
defmodule M do
require Integer
def even?(n) do
if Integer.is_even(n), do: :yes, else: :no # is_even is a macro
end
end
# Logger is a macro module, so require it first:
require Logger
Logger.info("starting up")
# Kernel is auto-required; its macros (if, unless, def) are always usable.use
use Module invokes Module's __using__/1 macro at compile time, which can inject any code (callbacks, defaults, requires). It's a flexible extension point—frameworks like GenServer, Phoenix, and Ecto build on it.
# use invokes the module's __using__/1 macro - a hook for setup:
defmodule MyServer do
use GenServer # injects callbacks & defaults
end
# implementing __using__/1 in your own module:
defmodule Tracer do
defmacro __using__(_opts) do
quote do
def traced_call(x), do: IO.inspect(x)
end
end
end
defmodule Demo do
use Tracer
end
Demo.traced_call(42) # prints 42
# always check the docs to see what use injects.Module Attributes
As Constants
Module attributes prefixed with @ act as compile-time constants. They're evaluated when the module is compiled and inlined at each use. Reading an attribute multiple times uses the value current at that point in the module.
defmodule AppConfig do
@max_retries 5
@timeout_ms 5000
@env Mix.env() # evaluated at compile time
def retry_count, do: @max_retries
def timeout, do: @timeout_ms
def call do
if @env == :prod, do: :prod_call, else: :dev_call
end
end@moduledoc and @doc
@moduledoc documents the module, @doc documents the next function/macro. Both are shown by h in IEx and by ExDoc. @doc false hides a function from documentation. @spec adds a type signature (used by Dialyzer and ExDoc).
defmodule Greeter do
@moduledoc """
Greets people.
Use `hello/1` to say hello.
"""
@doc "Says hello to `name`."
@spec hello(String.t()) :: String.t()
def hello(name), do: "Hello, #{name}!"
@doc false # hide from docs
def helper, do: :ok
end
# in IEx: h Greeter, h Greeter.helloAccumulating Attributes
An attribute accumulates values if you read it before reassigning (each registration prepends to the previous list). This is how many frameworks (Plug, Phoenix routes, Ecto schemas) collect declarations.
defmodule Plugin do
@callbacks []
# accumulate by re-reading and prepending:
defmacro register(name) do
quote do
@callbacks [unquote(name) | @callbacks]
end
end
register(:init)
register(:start)
def all, do: @callbacks
end
Plugin.all() # [:start, :init]@behaviour
@behaviour Module declares that this module implements another module's @callback specs (an interface). @impl true marks a function as implementing a callback and lets the compiler check arity/return type. Missing callbacks produce compile-time warnings.
# a behaviour defines callbacks other modules should implement:
defmodule Parser do
@doc "Parses input into a data structure."
@callback parse(String.t()) :: {:ok, term()} | {:error, term()}
end
defmodule JSONParser do
@behaviour Parser
@impl true
def parse(str), do: Jason.decode(str)
end
# if a callback is missing, the compiler warns.@impl and @callback
@impl true (or @impl GenServer) marks a function as a callback implementation, enabling the compiler to verify it matches a known callback and to warn about typos. Use @callback to declare an interface; @impl to fulfil it.
defmodule MyGenServer do
use GenServer
# @impl marks callback implementations:
@impl true
def init(state), do: {:ok, state}
@impl true
def handle_call(:get, _from, state), do: {:reply, state, state}
# without @impl, you get a warning if the function
# doesn't override a known callback.
end
# defining your own callbacks:
defmodule Sorter do
@callback compare(a :: term(), b :: term()) :: boolean()
endRecursion
Basic Recursion
Elixir has no loops—recursion is the way to iterate. Define a base case (empty list) and a recursive case that processes the head and recurses on the tail. This mirrors the structure of lists.
defmodule MyList do
# base case + recursive case:
def sum([]), do: 0
def sum([head | tail]), do: head + sum(tail)
def len([]), do: 0
def len([_ | tail]), do: 1 + len(tail)
end
MyList.sum([1, 2, 3]) # 6
MyList.len([:a, :b]) # 2
# recursion replaces loops; pattern matching handles the base case.Tail Recursion
A tail-recursive function makes the recursive call its last operation, so BEAM applies tail-call optimization (constant stack). Use an accumulator to defer computation. Non-tail recursion (head + recurse) grows the stack.
# tail-recursive: the recursive call is the LAST operation,
# so BEAM reuses the stack frame (no growth):
defmodule MyList do
def sum(list), do: sum(list, 0)
# accumulator carries the running total:
defp sum([], acc), do: acc
defp sum([head | tail], acc), do: sum(tail, acc + head)
end
MyList.sum([1, 2, 3, 4]) # 10
# contrast: head + sum(tail) is NOT tail-recursive
# (the + waits for the recursive result).Accumulator Pattern
The accumulator pattern threads a running result through recursive calls, enabling tail recursion. Building a list with prepend then reversing at the end is O(n) and the idiomatic tail-recursive map.
defmodule MyList do
# reverse using an accumulator (tail-recursive):
def reverse(list), do: reverse(list, [])
defp reverse([], acc), do: acc
defp reverse([h | t], acc), do: reverse(t, [h | acc])
# map with accumulator (built reversed, then reversed back):
def map(list, fun), do: map(list, fun, []) |> reverse()
defp map([], _fun, acc), do: acc
defp map([h | t], fun, acc), do: map(t, fun, [fun.(h) | acc])
end
MyList.reverse([1, 2, 3]) # [3, 2, 1]Tree Recursion
Tree recursion handles nested structures by recursing into both the head (when it's a list) and the tail. List.flatten/1 and Deep.count_leaves/1 are classic examples. Not tail-recursive, but fine for shallow nesting.
defmodule Deep do
# count leaves of a nested list:
def count_leaves([]), do: 0
def count_leaves([head | tail]) when is_list(head) do
count_leaves(head) + count_leaves(tail)
end
def count_leaves([_head | tail]), do: 1 + count_leaves(tail)
# flatten any nesting:
def flatten([]), do: []
def flatten([h | t]) when is_list(h), do: flatten(h) ++ flatten(t)
def flatten([h | t]), do: [h | flatten(t)]
end
Deep.count_leaves([1, [2, [3]], 4]) # 4Recursive Map
Map transforms the head and prepends to the mapped tail; filter optionally includes the head. Both build results in order without reversing. For production, prefer the optimized Enum functions.
defmodule MyList do
# recursive map:
def map([], _fun), do: []
def map([head | tail], fun), do: [fun.(head) | map(tail, fun)]
# recursive filter:
def filter([], _fun), do: []
def filter([h | t], fun) do
if fun.(h), do: [h | filter(t, fun)], else: filter(t, fun)
end
end
MyList.map([1, 2, 3], &(&1 * 2)) # [2, 4, 6]
MyList.filter([1, 2, 3, 4], &(rem(&1, 2) == 0)) # [2, 4]Recursive Reduce
reduce threads an accumulator through recursive calls—it's tail-recursive and handles large lists efficiently. This is essentially how Enum.reduce is implemented. The accumulator holds the running result.
defmodule MyList do
# left fold, recursive and tail-recursive:
def reduce([], acc, _fun), do: acc
def reduce([h | t], acc, fun), do: reduce(t, fun.(h, acc), fun)
end
MyList.reduce([1, 2, 3, 4], 0, &+/2) # 10
# the recursive call is the LAST operation,
# so this runs in constant stack space even for huge lists.
# This is essentially how Enum.reduce is implemented.Processes
spawn
BEAM processes are lightweight (kilobytes; millions can run). spawn/1 runs a function in a new process. Unlike OS threads, they're cheap and isolated—share nothing, communicate by message passing.
# spawn/1 creates a lightweight BEAM process:
pid = spawn(fn -> IO.puts("hello from process") end)
# prints "hello from process"; the process is now dead
# spawn/3 with module/function/args:
pid = spawn(Enum, :map, [[1, 2, 3], &(&1 * 2)])
# process info:
Process.alive?(pid) # false (already finished)
self() # current process PIDsend and receive
send/2 puts a message in a process's mailbox; receive/1 pattern-matches messages out. Messages queue until matched. The after clause adds a timeout—after 0 means non-blocking. Mailbox order is preserved per match.
# send/2 puts a message in a pid's mailbox:
pid = spawn(fn ->
receive do
{:hello, from} -> send(from, {:hi, self()})
{:bye, _} -> :ok
end
end)
send(pid, {:hello, self()})
# receive in the current process:
receive do
{:hi, pid} -> IO.puts("got hi from #{inspect(pid)}")
after
1000 -> :timeout
endProcess State Loop
A long-running process keeps state by recursing in a receive loop—each handler calls loop(new_state) as its tail call. This is how GenServer works under the hood. State is private to the process; no locks needed.
# a process that keeps state via tail recursion:
defmodule Counter do
def start(n), do: spawn(fn -> loop(n) end)
defp loop(state) do
receive do
{:inc, by} -> loop(state + by)
{:get, from} ->
send(from, {:count, state})
loop(state)
:stop -> :ok
end
end
end
c = Counter.start(0)
send(c, {:inc, 5})
send(c, {:get, self()})
receive do {:count, n} -> n end # 5Links and Termination
spawn_link/1 links two processes: if one exits abnormally, the other does too (Elixir's 'let it crash' philosophy). Set Process.flag(:trap_exit, true) to convert exits into {:EXIT, pid, reason} messages instead of dying.
# spawn_link: linked processes die together (fault tolerance):
pid = spawn_link(fn ->
receive do
:boom -> raise "crash"
end
end)
send(pid, :boom)
# BOTH processes die (the linked one raises, this one exits too)
# trap exits to handle instead of dying:
Process.flag(:trap_exit, true)
spawn_link(fn -> exit(:kaboom) end)
receive do
{:EXIT, from, reason} -> IO.puts("#{inspect(from)} died: #{reason}")
endMonitors
Process.monitor/1 watches a process one-way: when it dies, you get {:DOWN, ref, :process, pid, reason} without dying yourself. Use monitors when you want to observe without coupling lifecycles. Demonitor with Process.demonitor/1.
# monitors are one-way and don't kill the watcher:
pid = spawn(fn -> exit(:boom) end)
ref = Process.monitor(pid)
receive do
{:DOWN, ^ref, :process, ^pid, reason} ->
IO.puts("monitored process died: #{reason}")
end
# prints "monitored process died: boom"
# unlike links, the current process keeps running.
# monitors are asymmetric and unidirectional.
Process.demonitor(ref) # stop watching earlyTimeouts
The after clause in receive sets a timeout in ms; after 0 does a non-blocking check (returns immediately if no match). This enables polling, flush loops, and timeouts. Timeouts are crucial to avoid hanging forever.
# receive with after for a timeout:
receive do
{:data, x} -> x
after
1000 -> :no_response # 1 second
end
# after 0 = non-blocking check:
receive do
msg -> {:got, msg}
after
0 -> :empty
end
# flush helper (drain the mailbox):
def flush do
receive do
_ -> flush()
after
0 -> :ok
end
endOTP - GenServer
Defining a GenServer
use GenServer sets up the behaviour and injects defaults. Implement callbacks: init/1, handle_call/3 (sync), handle_cast/2 (async), handle_info/2 (other messages). @impl true marks each callback for compile-time checking.
defmodule Counter do
use GenServer
# callback: initial state
@impl true
def init(initial), do: {:ok, initial}
# synchronous (call) - returns a reply
@impl true
def handle_call(:get, _from, state), do: {:reply, state, state}
# asynchronous (cast) - no reply
@impl true
def handle_cast({:inc, n}, state), do: {:noreply, state + n}
endStarting a GenServer
GenServer.start_link/3 spawns the server (calling init/1) and links it to the caller. Register a name with name: to call it by atom instead of PID. In real apps, start servers under a Supervisor, not directly.
# start_link links the server to the caller (named or unnamed):
{:ok, pid} = GenServer.start_link(Counter, 0, name: MyCounter)
# register via the module name:
{:ok, pid} = GenServer.start_link(Counter, 0, name: __MODULE__)
# starting under a Supervisor (typical in real apps):
children = [
{Counter, 0}
]
Supervisor.start_link(children, strategy: :one_for_one)handle_call (sync)
handle_call/3 handles synchronous requests via GenServer.call/3. The from tuple lets you defer a reply with {:noreply, state} and GenServer.reply(from, reply) later. Returning {:stop, ...} terminates the server.
@impl true
def handle_call(:get, _from, state) do
{:reply, state, state}
end
@impl true
def handle_call({:add, n}, _from, state) do
new_state = state + n
{:reply, new_state, new_state}
end
# reply tuples:
# {:reply, reply, new_state}
# {:reply, reply, new_state, timeout | :hibernate}
# {:noreply, new_state} # reply later with GenServer.reply/2
# {:stop, reason, reply, new_state}handle_cast (async)
handle_cast/2 handles asynchronous fire-and-forget messages via GenServer.cast/2. There's no reply—return {:noreply, new_state} to continue, or {:stop, reason, new_state} to terminate. Use cast when the caller doesn't need a result.
@impl true
def handle_cast({:inc, n}, state) do
{:noreply, state + n}
end
@impl true
def handle_cast(:reset, _state) do
{:noreply, 0}
end
# cast returns immediately - the caller doesn't wait for a reply.
# useful for fire-and-forget updates, logging, side effects.
# return tuples:
# {:noreply, new_state}
# {:noreply, new_state, timeout | :hibernate}
# {:stop, reason, new_state}Client API
Convention: put the client API (functions calling GenServer.call/cast) in the same module as the server callbacks. Callers use Counter.get()/inc(), unaware of call/cast details. This hides the messaging behind a clean function interface.
defmodule Counter do
use GenServer
# client API (called by other code):
def start_link(initial) do
GenServer.start_link(__MODULE__, initial, name: __MODULE__)
end
def get, do: GenServer.call(__MODULE__, :get)
def inc(n \\ 1), do: GenServer.cast(__MODULE__, {:inc, n})
# server callbacks:
@impl true
def init(initial), do: {:ok, initial}
@impl true
def handle_call(:get, _from, state), do: {:reply, state, state}
@impl true
def handle_cast({:inc, n}, state), do: {:noreply, state + n}
endhandle_info
handle_info/2 handles 'raw' messages—plain sends, :DOWN monitor notifications, and Process.send_after/3 timeouts. call and cast have their own handlers; everything else lands here. Always implement it to avoid mailbox buildup.
defmodule Heartbeat do
use GenServer
@impl true
def init(_) do
send(self(), :tick)
{:ok, 0}
end
# handle non-call/cast messages (plain sends, monitors, timeouts):
@impl true
def handle_info(:tick, count) do
IO.puts("tick ##{count}")
Process.send_after(self(), :tick, 1000) # schedule next
{:noreply, count + 1}
end
def handle_info({:DOWN, _ref, :process, pid, reason}, state) do
IO.puts("#{inspect(pid)} died: #{reason}")
{:noreply, state}
end
endMix Tool
Creating a Project
mix new scaffolds a project with lib/, test/, mix.exs, and a .formatter.exs. --sup adds an Application module with a supervision tree (for OTP apps). Umbrella projects hold multiple sub-apps sharing deps.
# generate a new project:
$ mix new my_app
# creates:
# my_app/
# lib/my_app.ex
# lib/my_app/application.ex
# test/my_app_test.exs
# mix.exs
# .formatter.exs
# README.md
# with a supervision tree (OTP app):
$ mix new my_app --sup
# inside an umbrella project:
$ mix new apps/child_appmix.exs and Dependencies
mix.exs defines the project: app name, version, deps, and aliases. Dependencies are tuples of {name, version, opts}. only: restricts an env; runtime: false means compile-only. mix deps.get fetches; mix deps.tree visualizes.
defp deps do
[
{:phoenix, "~> 1.7"},
{:ecto_sql, "~> 3.10"},
{:jason, "~> 1.4"},
{:ex_doc, "~> 0.30", only: :dev, runtime: false},
{:credo, "~> 1.7", only: [:dev, :test], runtime: false}
]
end
# install:
$ mix deps.get
# list the dependency tree:
$ mix deps.tree
# why is a dependency included?
$ mix deps.unlock --unusedCommon Mix Commands
Common tasks: deps.get fetches deps, compile builds the project, iex -S mix opens IEx with the project & deps loaded, test runs ExUnit, format auto-formats. mix help lists all tasks; mix help TASK shows details.
$ mix new app # scaffold a project
$ mix deps.get # fetch dependencies
$ mix deps.compile # compile dependencies
$ mix compile # compile the project
$ mix run # start the app and keep it running
$ mix run -e "MyApp.hello()" # run an expression
$ iex -S mix # IEx with the project loaded
$ mix test # run the test suite
$ mix format # format source files
$ mix phx.server # (Phoenix) start the web server
$ mix help # list all available tasksMix Tasks
Custom tasks live in Mix.Tasks.* modules and use Mix.Task. The module name (after Mix.Tasks.) becomes the command (dots become underscores). @shortdoc shows up in mix help. Mix.shell() provides user I/O.
# define a custom task module:
defmodule Mix.Tasks.MyApp.Hello do
use Mix.Task
@shortdoc "Says hello"
def run(_args) do
Mix.shell().info("Hello from my task!")
end
end
# run it:
$ mix my_app.hello
# Hello from my task!Environments
Mix has three environments: :dev (default), :test, :prod. Mix.env() returns the current one. config/{env}.exs overrides config/config.exs per environment. Set MIX_ENV at build time (e.g., MIX_ENV=prod mix phx.server).
# mix.exs uses Mix.env() to branch per environment:
def project do
[
app: :my_app,
version: "0.1.0",
elixir: "~> 1.15",
start_permanent: Mix.env() == :prod, # permanent in prod
deps: deps()
]
end
# config/config.exs:
import Config
config :my_app, key: :default
# config/dev.exs, config/prod.exs, config/test.exs override per env.
# select env at build time:
# MIX_ENV=prod mix compile
# MIX_ENV=prod mix phx.serverExUnit Testing
Basic Tests
use ExUnit.Case brings in test/2, assert/1, and friends. async: true runs the test module concurrently with others (avoid for tests sharing global state). assert_raise/2 catches expected exceptions. Run with mix test.
# test/my_app_test.exs
defmodule MyAppTest do
use ExUnit.Case, async: true
test "addition" do
assert 1 + 1 == 2
end
test "lists have a head" do
[head | _] = [1, 2, 3]
assert head == 1
end
test "raises on missing key" do
assert_raise KeyError, fn -> %{a: 1}.b end
end
end
# run: mix testdescribe Blocks
describe groups related tests under a shared prefix, improving readability and test output. Each describe block can have its own setup. Tests inside a describe are typically not async with each other but are with other modules.
defmodule UserTest do
use ExUnit.Case
describe "name validation" do
test "requires a name" do
assert User.new(%{}) |> valid?() == false
end
test "accepts a non-empty name" do
assert User.new(%{name: "A"}) |> valid?() == true
end
end
describe "age" do
test "must be positive" do
assert {:error, _} = User.new(%{age: -1})
end
end
endsetup and setup_all
setup runs before each test, setup_all once per module. Both can return {:ok, key: val} to add to the test context, which is then pattern-matched in the test signature. on_exit/1 registers cleanup that always runs, even on failure.
defmodule DbTest do
use ExUnit.Case, async: false
# runs once before each test; context is merged into the test:
setup do
conn = open_conn()
on_exit(fn -> close_conn(conn) end)
{:ok, conn: conn}
end
# runs once for the whole module:
setup_all do
{:ok, schema: create_schema()}
end
test "uses conn", %{conn: conn} do
assert query(conn, "SELECT 1") == 1
end
endAssertions
assert checks its argument is truthy (with great error output on failure). refute is the inverse. assert_in_delta compares floats within a tolerance. Pattern assertions (assert {:ok, v} = ...) both check and bind.
assert 1 + 1 == 2
refute 1 == 2 # opposite of assert
assert_in_delta 3.14, 3.1415, 0.01 # floats within tolerance
assert_raise ArgumentError, fn -> hd([]) end
# pattern assertions (both check and bind):
assert {:ok, val} = parse("1")
assert %User{name: "A"} = build_user()
# catch exits/throws:
assert catch_exit(exit(:boom)) == :boom
# enum/map assertions:
assert length([1, 2]) == 2
assert user.age > 18Async Tests
async: true runs the test module concurrently with others—safe when tests don't share mutable state. start_supervised/1 starts a process tied to the test's lifecycle and tears it down automatically. Default to async: true; disable only when needed.
defmodule CounterTest do
use ExUnit.Case, async: true # concurrent with other modules
# each test gets its own Counter process, so no shared state:
setup do
{:ok, pid} = start_supervised(Counter)
%{pid: pid}
end
test "increments", %{pid: pid} do
Counter.inc(pid)
assert Counter.get(pid) == 1
end
test "starts at zero", %{pid: pid} do
assert Counter.get(pid) == 0
end
endRelated Elixir snippets
Copy-paste ready code for common tasks.
Pattern Matching
Pattern matching is core to Elixir — used everywhere.
Pipe Operator
Chain function calls with the |> pipe operator.
Processes and Messages
Spawn lightweight processes and send messages.
GenServer
Build stateful server processes with GenServer behaviour.
Supervisors and OTP
Build fault-tolerant supervision trees.
Protocols and Enums
Polymorphism via protocols and the Enum module.
Enum and Stream Operations
Functional collection operations in Elixir.
Metaprogramming with Macros
Write code that writes code at compile time.
Was this helpful?