DevTools

Cheatsheet Elixir

Linguagem funcional na BEAM VM para aplicações concorrentes e escaláveis

Back to languages
Elixir
76 cards found
Categories:
Versions:

Advanced Features


8 cards
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"
end

A 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)
end

defimpl 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
end

The 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
# => 4

A 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"]
end

Module 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 conflicts

The 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> examples

The @moduledoc documents the module and @doc documents functions. Examples with iex> become doctests validated automatically by ExUnit.

Basic


10 cards
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 one

The |> (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>>      # bitstring

Elixir 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 binaries

Strings 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


9 cards
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)   # 10

Functions 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=20

Destructuring 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}"
end

The ^ (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


9 cards
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")   # 5

A 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 options

A 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)   # true

Maps (%{}) 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 type

A 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 returns

For 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


9 cards
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


9 cards
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
end

The 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
end

The 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)
end

A 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


8 cards
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}"
end

case 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, KeyError

try/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)   # 120

Recursion 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 else

with 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


7 cards
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
end

A 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
end

An 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)}")
end

A 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


7 cards
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
end

The 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.Server

A 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
end

ExUnit is the testing framework. Tests use test with assert/refute and run with mix test. The assert_raise checks exceptions.