Cheatsheet Elixir
Linguagem funcional na BEAM VM para aplicações concorrentes e escaláveis
Elixir
Advanced Features
Protocols
# Define a protocol:
defprotocol Describable do
def describe(data)
end
# Implement for types:
defimpl Describable, for: BitString do
def describe(s), do: "String: #{s}"
end
defimpl Describable, for: Integer do
def describe(n), do: "Number: #{n}"
end
Describable.describe("hello") # "String: hello"
Describable.describe(42) # "Number: 42"A protocol defines ad-hoc polymorphism: declare a function with defprotocol and implement it per type with defimpl. The right function is chosen at runtime by the data type.
Behaviours
# Define a behaviour (contract):
defmodule Worker do
@callback process(data :: any) :: {:ok, any}
@callback name() :: String.t()
end
# Implement:
defmodule MyWorker do
@behaviour Worker
@impl true
def process(d), do: {:ok, d}
@impl true
def name, do: "MyWorker"
endA behaviour defines a contract of callbacks with @callback. The module that adopts it uses @behaviour and marks each implementation with @impl true, checked at compile time.
defimpl and Any
# For structs:
defimpl Describable, for: User do
def describe(u), do: "User: #{u.name}"
end
# Several types at once:
defimpl Describable, for: [List, Tuple] do
def describe(d), do: "Collection: #{inspect(d)}"
end
# Fallback for any type:
defimpl Describable, for: Any do
def describe(d), do: inspect(d)
enddefimpl implements the protocol for a type (or several, with a list). The for: Any defines a fallback for types without a specific implementation, avoiding errors.
use and __using__
# use injects code from a module:
defmodule MyHelper do
defmacro __using__(_opts) do
quote do
import MyHelper
@before_compile MyHelper
end
end
end
# Usage:
defmodule App do
use MyHelper
# now it has the injected code
endThe use calls the __using__ macro of a module, which injects code (imports, callbacks, etc.) into the current module. That is how GenServer and Supervisor work under the hood.
Macros
defmodule Logger do
defmacro log(expr) do
quote do
IO.puts("[LOG] #{inspect(unquote(expr))}")
unquote(expr)
end
end
end
require Logger
Logger.log(2 + 2)
# [LOG] 4
# => 4A macro (with defmacro) receives code and returns code, executed at compile time. The quote creates the AST and unquote inserts values. It requires require to be used.
Module Attributes
defmodule Config do
# Module attributes (@name):
@version "1.0.0"
@timeout 5000
def version, do: @version
# Accumulator (register several):
Module.register_attribute(__MODULE__,
:routes, accumulate: true)
@route "/home"
@route "/about"
def routes, do: @routes # ["/about", "/home"]
endModule attributes (@name) are constants evaluated at compile time. They are used for configuration, documentation and, with accumulate: true, to register lists of values.
quote and unquote
# quote: turns code into AST
quote do
1 + 2
end
# {:+, [], [1, 2]}
# unquote: inserts a value into the AST
x = 5
quote do
unquote(x) + 10
end
# {:+, [], [5, 10]}
# Macro hygiene avoids name conflictsThe quote converts code into its AST representation (syntax tree). The unquote injects a value inside a quote. They are the foundation of metaprogramming in Elixir.
Custom Sigils and Docs
# Documentation with @doc and @moduledoc:
defmodule Math do
@moduledoc "Math functions"
@doc """
Doubles a number.
## Examples
iex> Math.double(2)
4
"""
def double(x), do: x * 2
end
# Doctests:
# ExUnit validates the iex> examplesThe @moduledoc documents the module and @doc documents functions. Examples with iex> become doctests validated automatically by ExUnit.
Basic
Variables and Immutability
name = "Elixir" age = 30 active = true pi = 3.14 # Reassignment creates a new binding: x = 1 x = 2 # x is now 2 (does not mutate 1) # Everything is immutable: # operations return new values
Variables are assigned with = and all data is immutable. Reassigning a variable creates a new binding; the original values are never changed.
Pipe Operator
# Without pipe (nested):
String.upcase(String.trim(" hello "))
# With pipe (|>):
" hello "
|> String.trim()
|> String.upcase()
|> String.reverse()
# The result of each step is the
# first argument of the next oneThe |> (pipe) operator passes the result of the previous expression the the first argument of the next. It makes transformation pipelines readable, avoiding nested calls.
Sigils
# ~w: word list
~w(word1 word2) # ["word1", "word2"]
~w(a b c)a # [:a, :b, :c] (atoms)
# ~r: regular expression
~r/\d+/ # digit regex
Regex.run(~r/(\d+)/, "abc123") # ["123", "123"]
# ~s: string with interpolation
~s(hello #{name})
# ~D, ~T, ~N: dates and time
~D[2024-01-15]Sigils start with ~ and create values with special syntax: ~w (word lists), ~r (regex), ~s (strings) and ~D (dates). Modifiers like a change the type.
Basic Types
# Primitive types:
42 # integer
3.14 # float
"text" # string (binary)
?a # char (97)
true # boolean
:atom # atom
# Structures:
{1, 2, 3} # tuple
[1, 2, 3] # list
%{a: 1} # map
<<1, 2>> # bitstringElixir has integers, floats, strings (binaries), atoms (:name), booleans and structures like tuples, lists and maps.
IO (input/output)
# Print with newline:
IO.puts("Hello World")
# Inspection (debug, shows structure):
IO.inspect(%{a: 1, b: 2})
IO.inspect(list, label: "Data")
# Read input:
name = IO.gets("Name? ")
# Inspect returns a string (does not print):
inspect([1, 2, 3]) # "[1, 2, 3]"IO.puts prints text with a line break and IO.inspect shows the representation of any value (great for debugging). IO.gets reads a line from stdin.
Operators
# Arithmetic: 2 + 3 # 5 10 - 4 # 6 3 * 7 # 21 10 / 2 # 5.0 (always float) div(10, 3) # 3 (integer) rem(10, 3) # 1 (remainder) # Comparison: 1 == 1 # true 1 != 2 # true 1 === 1.0 # false (strict: int vs float) # Logical (booleans only): true and false # false true or false # true not true # false
Operators include + - * / (division / always returns a float; use div/rem for integers). The strict comparison === distinguishes 1 from 1.0.
Atoms
# Atom: constant whose name is its value
:ok
:error
:name
# Booleans are atoms:
true == :true # true
false == :false # true
nil == :nil # true
# Atoms with spaces:
:"hello world"
# Common use in result tuples:
{:ok, value}
{:error, reason}An atom is a constant whose name is the value itself (starts with :). The booleans true/false and nil are atoms. They are widely used in {:ok, _} and {:error, _} tuples.
Strings
s = "Hello World"
String.upcase(s) # "HELLO WORLD"
String.downcase(s) # "hello world"
String.length(s) # 11
String.trim(" hi ") # "hi"
String.split(s, " ") # ["Hello", "World"]
String.replace(s, "World", "Elixir")
String.contains?(s, "World") # true
# Strings are UTF-8 binariesStrings are UTF-8 binaries. The String module offers operations like upcase, split, replace and contains?, all immutable (they return new strings).
Modules
defmodule Greeting do
def hello(name) do
"Hello, #{name}!"
end
# Private function:
defp secret do
"internal"
end
end
# Call:
Greeting.hello("World") # "Hello, World!"Code is organized into modules with defmodule. Public functions are defined with def and private ones with defp (only visible inside the module).
Interpolation and Concatenation
name = "Anna"
age = 30
# Interpolation with #{}:
"Hello, #{name}!" # "Hello, Anna!"
"2 + 2 = #{2 + 2}" # "2 + 2 = 4"
# Concatenation:
"Hello" <> " " <> "World" # "Hello World"
# Multiline (heredoc):
text = """
Line 1
Line 2
"""Interpolation #{expr} inserts values into a string. The <> operator concatenates binaries. Multiline strings use triple quotes (heredoc).
Functions
def and defp
defmodule Math do
# Long body:
def double(x) do
x * 2
end
# Short body (one-liner):
def triple(x), do: x * 3
# Private (module only):
defp helper(x), do: x + 1
end
Math.double(5) # 10Functions are defined with def (public) or defp (private). The do: form is used for one-line bodies; the do...end block for multiple lines.
Guards
def classify(age) when age < 12 do "Child" end def classify(age) when age < 18 do "Teenager" end def classify(_), do: "Adult" # Guard functions: def process(x) when is_integer(x), do: "int" def process(x) when is_list(x), do: "list" # is_atom, is_binary, is_number, >, <, ==
Guards (when) add conditions to clauses. They use allowed functions like is_integer, is_list, is_atom and comparison operators.
Default Args and Clauses
# Argument with default (\\):
def greet(name, punct \\ "!") do
"Hello, #{name}#{punct}"
end
greet("Anna") # "Hello, Anna!"
greet("Anna", "?") # "Hello, Anna?"
# Multiple clauses of the same function:
def fact(0), do: 1
def fact(n), do: n * fact(n - 1)Optional arguments use \ to define a default value. A function can have multiple clauses (same name/arity), tried in order until one matches.
Pattern Matching in Parameters
# Matches by structure:
def area({:circle, r}), do: :math.pi * r * r
def area({:rect, w, h}), do: w * h
area({:circle, 2}) # 12.56...
area({:rect, 3, 4}) # 12
# Multiple clauses per pattern:
def describe([]), do: "empty"
def describe([_ | _]), do: "with elements"Parameters do pattern matching: each clause matches a specific shape. Elixir tries the clauses in order until one matches, enabling elegant polymorphic functions.
Anonymous Functions (fn)
# Define: double = fn x -> x * 2 end sum = fn a, b -> a + b end # Call with a dot: double.(5) # 10 sum.(3, 4) # 7 # Multiple clauses: kind = fn x when is_integer(x) -> "int" x when is_binary(x) -> "string" end kind.(42) # "int"
Anonymous functions are created with fn ... end and called with a dot (double.(5)). They can have multiple clauses with pattern matching and guards.
Destructuring
# Tuples:
{x, y, z} = {1, 2, 3} # x=1, y=2, z=3
# Lists (head/tail):
[head | tail] = [1, 2, 3, 4] # head=1, tail=[2,3,4]
# Maps:
%{name: n} = %{name: "Anna", age: 30} # n="Anna"
# Ignore with _:
{_, second, _} = {10, 20, 30} # second=20Destructuring extracts parts of structures on the left side of =. _ ignores positions and [head | tail] separates the first element from the rest of the list.
Shorthand &()
# &() is a shortcut for fn:
double = &(&1 * 2) # fn x -> x * 2 end
sum = &(&1 + &2) # fn a, b -> a + b end
# &1, &2... are the parameters:
Enum.map([1, 2, 3], &(&1 * 2)) # [2, 4, 6]
# Mix with text:
greet = &"Hello, #{&1}!"
greet.("Anna") # "Hello, Anna!"The &() syntax is a short form of anonymous function, where &1, &2 represent the parameters by position. It is ideal for small functions in Enum.
Pin Operator (^)
x = 1
# Without pin: re-bind (x becomes 2)
x = 2
# With pin: matches against the current value
^x = 2 # OK, x is 2
^x = 3 # MatchError! (x is 2, not 3)
# Useful in parameters and case:
case {1, 2} do
{^x, y} -> "x matched, y=#{y}"
endThe ^ (pin) operator prevents re-binding and forces comparison against the current value of the variable. Without it, = reassigns; with it, it matches against the existing value.
Function Capture
# Capture a named function:
upcase = &String.upcase/1
upcase.("hello") # "HELLO"
# Module.function/arity notation:
Enum.map(["a", "b"], &String.upcase/1)
# ["A", "B"]
# Partial capture:
double = &Kernel.*/2
# more common: &(&1 * 2)The capture &Module.function/arity turns a named function into an anonymous one (e.g. &String.upcase/1). The arity is the number of arguments.
Data Structures
Lists
list = [1, 2, 3, 4] # Cons constructor (|): [0 | list] # [0, 1, 2, 3, 4] # Access: hd(list) # 1 (head) tl(list) # [2, 3, 4] (tail) length(list) # 4 # Lists are linked: # prepending is O(1)
Lists are linked lists: prepending with [x | list] is efficient (O(1)). hd gives the head, tl the rest and length the size.
Tuples
# Tuple: fixed size, index access
point = {3, 4}
elem(point, 0) # 3
elem(point, 1) # 4
tuple_size(point) # 2
# Update a position:
put_elem(point, 1, 5) # {3, 5}
# Common use: return values
{:ok, data}
{:error, "failed"}Tuples have a fixed size and index access with elem. They are ideal for return values like {:ok, value}. Unlike lists, they are not iterable.
Binaries and Bitstrings
# Binary (byte sequence):
<<1, 2, 3>> # 3 bytes
"hello" # is a binary
# Size in bits:
<<x::8, y::8>> = <<10, 20>> # x=10, y=20
# Concatenate:
<<1, 2>> <> <<3>> # <<1, 2, 3>>
# Strings are binaries:
is_binary("hello") # true
byte_size("hello") # 5A binary (<< >>) is a byte sequence; a bitstring can have a size in bits. Strings are UTF-8 binaries. <<x::8>> specifies the size.
List Operations
a = [1, 2, 3] # Concatenation: a ++ [4, 5] # [1, 2, 3, 4, 5] # Difference: [1, 2, 3] -- [2] # [1, 3] # Membership: 3 in a # true 9 in a # false # Reversal: Enum.reverse(a) # [3, 2, 1]
++ concatenates two lists and -- removes elements. The in operator tests membership. These operations traverse the list, being O(n).
Keyword Lists
# List of {atom, value} pairs:
opts = [name: "App", debug: true]
# Equivalent to:
[{:name, "App"}, {:debug, true}]
# Access:
opts[:name] # "App"
Keyword.get(opts, :debug) # true
Keyword.get(opts, :port, 4000) # default
# Widely used for function optionsA keyword list is a list of {atom, value} tuples, written the [key: value]. It keeps order and allows repeated keys; it is common for function options.
Maps
person = %{name: "Anna", age: 30}
# Access:
person.name # "Anna" (atom keys only)
person[:age] # 30
Map.get(person, :email, "N/A") # "N/A"
# Keys:
Map.keys(person) # [:name, :age]
Map.values(person) # ["Anna", 30]
Map.has_key?(person, :name) # trueMaps (%{}) store unordered key-value pairs. With atom keys, access uses dot notation (person.name). Map.get accepts a default value.
Structs
defmodule User do
defstruct name: "", age: 0, active: true
end
# Create:
u = %User{name: "Anna", age: 30}
# Access:
u.name # "Anna"
# Update:
%{u | age: 31}
# A struct is a map with __struct__:
%User{} = u # pattern match by typeA struct is a map with fixed keys and default values, defined with defstruct. It guarantees the shape of the data and allows pattern matching by type (%User{}).
Updating Maps
person = %{name: "Anna", age: 30}
# Update syntax (existing keys only):
%{person | age: 31}
# Map.put (adds or updates):
Map.put(person, :email, "ana@mail.com")
# Map.update (with a function):
Map.update(person, :age, 0, &(&1 + 1))
# Map.delete:
Map.delete(person, :age)Maps are immutable. The %{m | key: value} syntax updates existing keys; Map.put adds or replaces, Map.update applies a function and Map.delete removes.
Nested Access
data = %{user: %{name: "Anna", address: %{city: "Porto"}}}
# get_in: reads a path
get_in(data, [:user, :address, :city])
# "Porto"
# update_in: updates a path
update_in(data.user.age, &(&1 + 1))
# put_in: sets a path
put_in(data[:user][:email], "ana@mail.com")
# pop_in: removes and returnsFor nested structures, get_in reads a key path, update_in applies a function on that path and put_in sets a value — all immutably.
Enum e Stream
map, filter, reduce
# map: transforms each element Enum.map([1, 2, 3], &(&1 * 2)) # [2, 4, 6] # filter: keeps the ones that pass Enum.filter([1, 2, 3, 4], &(&1 > 2)) # [3, 4] # reduce: aggregates with an accumulator Enum.reduce([1, 2, 3], 0, &(&1 + &2)) # 6
Enum.map transforms each element, Enum.filter selects the ones that pass the predicate and Enum.reduce combines everything into a value with an initial accumulator.
Aggregation
Enum.sum([1, 2, 3, 4]) # 10 Enum.max([3, 1, 4]) # 4 Enum.min([3, 1, 4]) # 1 Enum.count([1, 2, 3]) # 3 # With a predicate: Enum.count([1, 2, 3, 4], &(&1 > 2)) # 2 # By a function: Enum.max_by(users, & &1.age) Enum.min_by(users, & &1.age)
These functions summarize collections: sum, max, min and count. count accepts a predicate and max_by/min_by use a criterion function.
Ranges and Comprehensions
# Range:
1..10
Enum.to_list(1..5) # [1, 2, 3, 4, 5]
Enum.sum(1..100) # 5050
# Comprehension (for):
for x <- 1..5, do: x * x
# [1, 4, 9, 16, 25]
# With a filter:
for x <- 1..10, even?(x), do: x
# Multiple generators:
for x <- 1..3, y <- 1..3, do: {x, y}A range (1..10) is a lazy sequence of integers. The for comprehension builds lists with generators (<-) and filters, similar to list comprehensions.
each and find
# each: side effect (does not transform) Enum.each([1, 2, 3], &IO.puts/1) # find: first one that passes Enum.find([1, 2, 3, 4], &(&1 > 2)) # 3 # find with default: Enum.find([1, 2], &(&1 > 10), :none) # :none # find_index: position Enum.find_index([10, 20, 30], &(&1 == 20)) # 1
Enum.each runs a side effect without returning a list. Enum.find returns the first element that satisfies the predicate (or a default) and find_index its position.
group_by and frequencies
# group_by: groups by a function
Enum.group_by(1..10, &rem(&1, 2))
# %{0 => [2,4,6,8,10], 1 => [1,3,5,7,9]}
Enum.group_by(users, & &1.city)
# frequencies: counts occurrences
Enum.frequencies(~w(a b a c a b))
# %{a: 3, b: 2, c: 1}
# frequencies_by:
Enum.frequencies_by(users, & &1.type)Enum.group_by groups elements into a map by a key function. Enum.frequencies counts how many times each value appears; frequencies_by counts by a criterion.
sort and uniq
Enum.sort([3, 1, 2]) # [1, 2, 3] Enum.sort([3, 1, 2], :desc) # [3, 2, 1] # sort_by: sorts by a function Enum.sort_by(users, & &1.age) Enum.sort_by(users, & &1.age, :desc) # uniq: removes duplicates Enum.uniq([1, 1, 2, 3, 3]) # [1, 2, 3] Enum.uniq_by(users, & &1.email)
Enum.sort sorts (with :desc for descending) and sort_by sorts by a function. Enum.uniq removes duplicates; uniq_by by a criterion.
any?, all?, member?
# any?: does any pass? Enum.any?([1, 2, 3], &(&1 > 2)) # true # all?: do all pass? Enum.all?([1, 2, 3], &(&1 > 0)) # true # member?: contains a value? Enum.member?([1, 2, 3], 2) # true # empty?: Enum.empty?([]) # true # take / drop: Enum.take([1, 2, 3, 4], 2) # [1, 2] Enum.drop([1, 2, 3, 4], 2) # [3, 4]
Enum.any? checks if any element passes, all? if all pass and member? if it contains a value. take/drop take away N elements.
chunk, zip, flat_map
# chunk_every: groups into blocks
Enum.chunk_every([1, 2, 3, 4, 5], 2)
# [[1, 2], [3, 4], [5]]
# zip: combines into pairs
Enum.zip([1, 2], [:a, :b]) # [{1, :a}, {2, :b}]
# flat_map: map + flatten
Enum.flat_map([[1, 2], [3, 4]], & &1)
# [1, 2, 3, 4]
# with_index:
Enum.with_index([:a, :b]) # [{:a, 0}, {:b, 1}]chunk_every groups into blocks of N, zip combines two collections into pairs and flat_map maps and flattens. with_index pairs each element with its index.
Streams (lazy)
# Stream: lazy evaluation
1..1_000_000
|> Stream.filter(&(rem(&1, 2) == 0))
|> Stream.map(&(&1 * &1))
|> Enum.take(5)
# [4, 16, 36, 64, 100]
# File stream (line by line):
File.stream!("big.txt")
|> Stream.map(&String.trim/1)
|> Stream.filter(&(&1 != ""))
|> Enum.to_list()A Stream is lazy: it only processes elements when needed (with Enum.take or to_list). It is ideal for large or infinite data, avoiding intermediate lists.
Processes
spawn
# Create a process:
pid = spawn(fn ->
IO.puts("Hello from another process")
end)
# pid is the identifier (#PID<0.123.0>)
Process.alive?(pid) # true/false
# spawn with module/function:
spawn(MyModule, :function, [arg1, arg2])
# Processes are lightweight (thousands OK)spawn creates a lightweight concurrent process and returns its pid. BEAM processes are isolated and cheap — it is common to have thousands running at once.
Task async/await
# Task: asynchronous computation task = Task.async(fn -> Process.sleep(1000) 42 end) # Do other work... # await blocks until the result: result = Task.await(task, 5000) # 42 # Several tasks in parallel: tasks = Enum.map(1..5, &Task.async(fn -> &1 * 2 end)) Enum.map(tasks, &Task.await/1)
A Task runs asynchronous work. Task.async starts and returns the task; Task.await blocks until it gets the result. Several tasks run in parallel.
GenServer Callbacks
defmodule Counter do
use GenServer
# Initialization:
def init(v), do: {:ok, v}
# Handles call (synchronous):
def handle_call(:get, _from, n) do
{:reply, n, n}
end
# Handles cast (asynchronous):
def handle_cast(:inc, n) do
{:noreply, n + 1}
end
endThe GenServer callbacks handle the messages: init sets the initial state, handle_call replies to call ({:reply, resp, state}) and handle_cast handles cast ({:noreply, state}).
send and receive
# Send a message:
send(pid, {:hello, "World"})
# Receive (blocks until it arrives):
receive do
{:hello, msg} -> IO.puts(msg)
{:bye, _} -> IO.puts("Bye")
_ -> IO.puts("Unknown")
end
# self() is the pid of the current process
send(self(), :ping)Processes communicate via messages: send sends to a pid and receive blocks until a matching message arrives. self() is the pid of the current process.
Task.async_stream
# Process a collection in parallel:
1..10
|> Task.async_stream(&process/1,
max_concurrency: 4)
|> Enum.map(fn {:ok, r} -> r end)
# timeout and on_timeout:
|> Task.async_stream(&slow/1,
timeout: 3000,
on_timeout: :kill_task)Task.async_stream processes a collection in parallel, preserving order. max_concurrency limits simultaneous processes; timeout controls the time per task.
receive with Timeout
receive do
{:result, value} -> value
after
5000 -> IO.puts("Timeout (5s)")
end
# after sets the maximum wait time
# (in milliseconds)
# 0 = do not block (check right away)
receive do
msg -> msg
after
0 -> :nothing
endThe after clause in receive sets a timeout in milliseconds; if no message arrives, the timeout branch runs. With 0, it checks without blocking.
Agent
# Agent: simple shared state
{:ok, pid} = Agent.start_link(fn -> 0 end)
# Read:
Agent.get(pid, & &1) # 0
# Update:
Agent.update(pid, &(&1 + 1)) # ok
Agent.get(pid, & &1) # 1
# Read and update:
Agent.get_and_update(pid, fn s ->
{s, s + 10}
end)
# With a name:
Agent.start_link(fn -> %{} end, name: MyState)An Agent holds shared state in a simple way. Agent.get reads, Agent.update modifies and get_and_update does both. Ideal for simple state.
Process (info and dictionary)
# Process information: Process.info(self()) Process.alive?(pid) # Process dictionary (local state): Process.put(:key, "value") Process.get(:key) # "value" # Links: Process.link(pid) Process.monitor(pid)
The Process module gives information (info, alive?) and a local dictionary (put/get). link links processes; monitor observes without propagating failures.
GenServer (call and cast)
defmodule Counter do
use GenServer
def start_link(init \\ 0) do
GenServer.start_link(__MODULE__, init,
name: __MODULE__)
end
# Synchronous API (replies):
def value, do: GenServer.call(__MODULE__, :get)
# Asynchronous API (no reply):
def increment, do: GenServer.cast(__MODULE__, :inc)
endA GenServer is a stateful process. GenServer.call is synchronous (waits for a reply) and GenServer.cast is asynchronous (does not wait). The public API encapsulates the messages.
Control and Errors
case
case {1, 2, 3} do
{x, y, z} when x > 0 -> "positive"
{0, _, _} -> "zero"
_ -> "other"
end
# With a function result:
case File.read("f.txt") do
{:ok, content} -> content
{:error, reason} -> "Error: #{reason}"
endcase pattern matches a value against several patterns, with optional guards. The _ is the default case. It is ideal for handling {:ok, _}/{:error, _} tuples.
try and rescue
try do
raise "Something failed!"
rescue
e in RuntimeError -> IO.puts(e.message)
e -> IO.puts("Other: #{inspect(e)}")
after
IO.puts("Always runs")
end
# rescue by exception type:
# RuntimeError, ArgumentError,
# ArithmeticError, KeyErrortry/rescue catches exceptions by type (e.g. RuntimeError). The after block always runs. In Elixir, {:error, _} tuples are preferred over exceptions.
cond
# cond: multiple conditions (if/elseif) cond do x > 10 -> "big" x > 5 -> "medium" true -> "small" # default end # Equivalent to nested ifs: # Each branch is a boolean expression # The final true is the else
cond evaluates several conditions in sequence, like an if/elseif. Each branch is a boolean expression; the final true works the the default case (else).
raise and Exceptions
# Raise an exception: raise "Simple error" raise ArgumentError, "invalid value" # Define a custom exception: defmodule MyError do defexception message: "default error" end raise MyError, message: "custom failure" # try/rescue catches them
raise throws an exception with a message or type. Custom exceptions are defined with defexception. In Elixir, exceptions are for truly exceptional situations.
if and unless
# if (short form): if x > 0, do: "pos", else: "neg" # if (block): if x > 0 do "positive" else "negative" end # unless (negated): unless x > 0 do "not positive" end # In Elixir, if is a macro/expression
if returns a value and has a short form (do:/else:) or a block form. unless is the negated if. Both are expressions that return the result of the executed branch.
Recursion
defmodule Math do
# Base case + recursive case:
def factorial(0), do: 1
def factorial(n) when n > 0 do
n * factorial(n - 1)
end
# Sum a list:
def sum([]), do: 0
def sum([h | t]), do: h + sum(t)
end
Math.factorial(5) # 120Recursion replaces loops in Elixir (there are no imperative for/while). You define a base case and a recursive case, often with pattern matching on lists.
with (happy path)
# Chain operations that can fail:
with {:ok, user} <- find_user(id),
{:ok, posts} <- get_posts(user) do
{:ok, posts}
else
{:error, reason} -> {:error, reason}
:not_found -> {:error, "does not exist"}
end
# If any <- fails, it jumps to elsewith chains operations on the "happy path": each <- does a match; if all match, the do block runs. If any fails, it jumps to the else.
Recursion with Accumulator
defmodule Math do
# Tail-recursive with accumulator:
def factorial(n), do: do_fact(n, 1)
defp do_fact(0, acc), do: acc
defp do_fact(n, acc) do
do_fact(n - 1, n * acc)
end
end
# The accumulator avoids growing the stack
# (tail-call optimization on the BEAM)Recursion with an accumulator (tail-recursive) is optimized on the BEAM: the result is passed the an argument, avoiding stack growth. It is the preferred form for performance.
OTP and Supervision
Supervisor
defmodule MyApp.Supervisor do
use Supervisor
def start_link(opts) do
Supervisor.start_link(__MODULE__, opts)
end
def init(_opts) do
children = [
Counter,
MyGenServer
]
Supervisor.init(children,
strategy: :one_for_one)
end
endA Supervisor manages child processes and restarts them if they fail. Children are listed in children and the strategy defines how to restart. It is the foundation of fault tolerance.
DynamicSupervisor
# Supervisor for dynamic children:
children = [
{DynamicSupervisor,
name: MyApp.DynSup,
strategy: :one_for_one}
]
# Start a child at runtime:
DynamicSupervisor.start_child(
MyApp.DynSup,
{Worker, arg}
)
# Count children:
DynamicSupervisor.count_children(MyApp.DynSup)The DynamicSupervisor supervises children created at runtime (not predefined). start_child adds processes dynamically — ideal for a variable number of workers.
Supervision Strategies
# :one_for_one - only the one that failed Supervisor.init(children, strategy: :one_for_one) # :one_for_all - all restart strategy: :one_for_all # :rest_for_one - the failed one and the following strategy: :rest_for_one # max_restarts / max_seconds: Supervisor.init(children, strategy: :one_for_one, max_restarts: 3, max_seconds: 5)
Strategies define restarting: :one_for_one (only the failed one), :one_for_all (all) and :rest_for_one (the failed one and the following). max_restarts limits restarts.
ETS
# ETS: in-memory table (mutable!)
table = :ets.new(:cache, [:set, :public])
# Insert:
:ets.insert(table, {:key, "value"})
:ets.insert(table, [{:a, 1}, {:b, 2}])
# Look up:
:ets.lookup(table, :key) # [{:key, "value"}]
# Delete:
:ets.delete(table, :key)
# Named table:
:ets.new(:cache, [:set, :named_table, :public])ETS (Erlang Term Storage) is a shared, mutable in-memory table, used the a high-performance cache. Types: :set, :ordered_set, :bag. Accessed via the :ets module.
Application
# mix.exs defines the application:
def application do
[
mod: {MyApp.Application, []},
extra_applications: [:logger]
]
end
# Application starts the tree:
defmodule MyApp.Application do
use Application
def start(_type, _args) do
children = [Counter]
Supervisor.start_link(children,
strategy: :one_for_one)
end
endAn Application is the entry point that starts the supervision tree. It is defined in mix.exs with mod: and implements the start/2 callback.
Links and Monitors
# Link: failure propagates (bidirectional)
Process.link(pid)
# spawn_link: creates already linked
spawn_link(fn -> task() end)
# Monitor: notifies without propagating
ref = Process.monitor(pid)
receive do
{:DOWN, ^ref, :process, ^pid, reason} ->
IO.puts("Died: #{inspect(reason)}")
endA link ties two processes: if one fails, the other dies too (propagation). A monitor only notifies the failure (a :DOWN message) without propagating, allowing you to react.
Registry
# Registry: naming dynamic processes
children = [
{Registry, keys: :unique, name: MyApp.Registry}
]
# Register a process:
Registry.register(MyApp.Registry, "user_1", nil)
# Look up the pid:
Registry.lookup(MyApp.Registry, "user_1")
# [{pid, value}]
# Send to all with a key:
Registry.dispatch(MyApp.Registry, "room", fn entries ->
for {pid, _} <- entries, do: send(pid, :msg)
end)The Registry lets you name and look up dynamic processes by key. With keys: :unique each key has one process; keys: :duplicate allows several (useful for pub/sub).
Mix and Tools
mix new
# Create a project: mix new my_app # With a supervision tree: mix new my_app --sup # Generated structure: # my_app/ # lib/my_app.ex # test/my_app_test.exs # mix.exs # README.md
mix new creates a new project with the standard structure. The --sup flag adds a supervision tree (Application). The mix.exs is the configuration file.
mix format
# Format the whole project:
mix format
# Check without changing (CI):
mix format --check-formatted
# Configuration in .formatter.exs:
[
inputs: ["{mix,.formatter}.exs",
"{config,lib,test}/**/*.{ex,exs}"],
line_length: 98
]mix format formats the code according to the official Elixir style. The --check-formatted validates without changing (useful in CI). Configuration lives in .formatter.exs.
mix.exs and Dependencies
defmodule MyApp.MixProject do
use Mix.Project
def project do
[app: :my_app, version: "0.1.0"]
end
def application do
[extra_applications: [:logger]]
end
defp deps do
[
{:phoenix, "~> 1.7"},
{:jason, "~> 1.4"}
]
end
endThe mix.exs configures the project: project (name/version), application (OTP apps) and deps (Hex dependencies). The version uses the ~> 1.7 format (compatible).
IEx (REPL)
# Start the REPL: iex iex -S mix # with the project # Help: h Enum.map # docs i "hello" # term info # Continue an incomplete expression: # Enter on an incomplete line continues # Recompile at runtime: recompile() # h() lists help, c() compiles a file
IEx is the interactive REPL. The iex -S mix loads the project. The h shows documentation, i inspects a term and recompile() recompiles at runtime.
mix Commands
# Dependencies: mix deps.get # download mix deps.update jason # update # Compile: mix compile # Run: mix run # script mix run -e "IO.puts(1)" # inline # Common tasks: mix format # format code mix test # tests mix help # list tasks
mix is the build tool: deps.get downloads dependencies, compile compiles, run executes, format formats and test runs the tests.
Project Structure
my_app/
lib/
my_app.ex # main module
my_app/
server.ex # MyApp.Server
test/
my_app_test.exs
test_helper.exs
config/
config.exs
mix.exs
.formatter.exs
# File name -> module:
# lib/my_app/server.ex -> MyApp.ServerA project is organized into lib/ (code), test/ (tests) and config/. The file path defines the module: lib/my_app/server.ex is MyApp.Server.
mix test and ExUnit
defmodule MathTest do
use ExUnit.Case
test "double of 2 is 4" do
assert Math.double(2) == 4
end
test "non-empty list" do
refute Enum.empty?([1, 2])
end
test "raises error" do
assert_raise ArgumentError, fn ->
raise ArgumentError
end
end
endExUnit is the testing framework. Tests use test with assert/refute and run with mix test. The assert_raise checks exceptions.