⏳
Loading cheatsheet...
Pattern matching, modules, processes, OTP, Ecto, Phoenix, concurrency, and functional programming.
# ── Basic Types ──
# Integers
42 # integer
0x1F # hexadecimal (31)
0o77 # octal (63)
0b1010 # binary (10)
1_000_000 # underscored for readability
# Floats
3.14
1.0e10
# Atoms (constants, their value is their name)
:ok
:error
:hello_world
# Booleans are atoms
true # :true
false # :false
nil # :nil (also falsy)
# Strings (UTF-8 encoded binaries)
"hello"
"he #{1 + 1}lo" # string interpolation => "he 2lo"
"hello
world" # escape sequences
~w(hello world) # sigil: ["hello", "world"]
~s(hello world) # sigil: "hello world"
~r/hello/i # sigil: regex
# Binaries
<<1, 2, 3>>
<<255>> # byte
<<1::size(2), 3::size(6)>> # bitstring
# Lists (linked lists)
[1, 2, 3]
[1, :two, "three"] # mixed types
[1 | [2 | [3 | []]]] # head | tail structure
[1, 2, 3] ++ [4, 5] # concatenation
[1, 2, 3] -- [2] # subtraction
# Tuples
{:ok, "result"}
{:error, "reason"}
elem({:a, :b, :c}, 0) # => :a
put_elem({:a, :b}, 1, :x) # => {:a, :x}
# Maps
%{name: "Alice", age: 30}
%{"key" => "value"}
map = %{a: 1, b: 2}
map[:a] # => 1
Map.put(map, :c, 3) # => %{a: 1, b: 2, c: 3}
Map.get(map, :d, "default") # => "default"
# Ranges
1..10 # inclusive
1..10//2 # step (1, 3, 5, 7, 9)
# ── Pattern Matching ──
# Match operator (=) is NOT assignment
{x, y} = {1, 2} # x = 1, y = 2
{a, b, c} = {1, 2, 3}
# Pin operator (^) prevents re-binding
x = 1
{x, ^x} = {2, 1} # x stays 1 (pinned), matches!
# {x, ^x} = {2, 2} # MatchError!
# Function head matching
case {1, 2, 3} do
{1, x, 3} -> "Got #{x}"
{4, 5, 6} -> "Not this"
_ -> "Catch-all"
end
# Guards in matches
case {:ok, 42} do
{:ok, val} when val > 0 -> "Positive: #{val}"
{:ok, val} when val < 0 -> "Negative: #{val}"
{:error, _} -> "Error"
end
# List matching
[head | tail] = [1, 2, 3] # head = 1, tail = [2, 3]
[first, second | rest] = [1, 2, 3, 4]| Type | Mutable? | Example |
|---|---|---|
| Integer | No | 42, 0xFF |
| Float | No | 3.14, 1.0e5 |
| Atom | No | :ok, :error |
| Boolean | No | true / false |
| String | No | "hello" |
| Binary | No | <<1, 2, 3>> |
| List | No | [1, 2, 3] |
| Tuple | No | {:ok, 1} |
| Map | No | %{a: 1} |
| PID | No | self() |
| Function | Yes (closure) | fn x -> x end |
| Reference | No | make_ref() |
| Operator | Arity | Description | |||||
|---|---|---|---|---|---|---|---|
| + | - | * | / | div | rem | 2 | Arithmetic |
| == | != | === | !== | 2 | Comparison | ||
| && | || | ! | 2/1 | Logical (strict) | |||
| and | or | not | 2/1 | Logical (strict bool) | |||
| < | > | <= | >= | 2 | Ordering | ||
| ++ | -- | 2 | List concat/sub | ||||
| in | 2 | Membership check | |||||
| |> | 2 | Pipe operator | |||||
| . | 2 | Function/Map access |
# ── Defining a Module ──
defmodule Math do
# Public function
def add(a, b), do: a + b
# Multi-clause function
def factorial(0), do: 1
def factorial(n) when n > 0 do
n * factorial(n - 1)
end
# Private function
defp double(x), do: x * 2
# Default argument
def greet(name, greeting \\ "Hello") do
"#{greeting}, #{name}!"
end
# Guard clause
def classify_age(age) when age < 13, do: :child
def classify_age(age) when age < 18, do: :teenager
def classify_age(age) when age < 65, do: :adult
def classify_age(_), do: :senior
# Function capturing
def adder do
&(&1 + &2) # fn a, b -> a + b end
end
end
# Calling functions
Math.add(1, 2) # => 3
Math.factorial(5) # => 120
Math.greet("Alice") # => "Hello, Alice!"
Math.greet("Bob", "Hi") # => "Hi, Bob!"
add = Math.adder()
add.(3, 4) # => 7
# ── Anonymous Functions (Closures) ──
sum = fn a, b -> a + b end
sum.(1, 2) # => 3
# Shorthand syntax
multiply = &(&1 * &2)
multiply.(3, 4) # => 12
# Closures capture scope
x = 10
adder = fn y -> x + y end
adder.(5) # => 15
# Named function with capture
Enum.map([1, 2, 3], &Math.factorial/1)
# ── Module Attributes ──
defmodule Config do
@version "1.0.0"
@max_retries 3
def version, do: @version
def max_retries, do: @max_retries
end
# ── Structs (extending Maps) ──
defmodule User do
defstruct [:name, :email, :age, roles: []]
def admin?(%User{roles: roles}) do
:admin in roles
end
def greet(%User{name: name}) do
"Hello, #{name}!"
end
end
user = %User{name: "Alice", email: "alice@example.com", age: 30}
user.name # => "Alice"
%User{user | age: 31} # update (returns new struct)
User.admin?(user) # => false
# ── Protocols (like interfaces) ──
defprotocol Stringable do
def to_string(value)
end
defimpl Stringable, for: Integer do
def to_string(n), do: Integer.to_string(n)
end
defimpl Stringable, for: User do
def to_string(%User{name: name}), do: name
end
Stringable.to_string(42) # => "42"
Stringable.to_string(user) # => "Alice"# ── Pipe Operator (|>) ──
# Pass result of one expression as first arg to next
[1, 2, 3, 4, 5]
|> Enum.map(&(&1 * 2))
|> Enum.filter(&(&1 > 4))
|> Enum.sum
# => 24 (6 + 8 + 10)
# Equivalently:
Enum.sum(Enum.filter(Enum.map([1,2,3,4,5], &(&1*2)), &(&1>4)))
# ── Enum Functions ──
# Transform
Enum.map([1, 2, 3], fn x -> x * 2 end) # [2, 4, 6]
Enum.flat_map([1, 2], fn x -> [x, x * 10] end) # [1, 10, 2, 20]
Enum.map_every(3, [1,2,3,4,5,6,7], &(&1 * 2)) # [2, 2, 3, 4, 10, 6, 7]
# Filter
Enum.filter([1,2,3,4,5], &(&1 > 3)) # [4, 5]
Enum.reject([1,2,3,4,5], &(&1 > 3)) # [1, 2, 3]
Enum.filter([1,2,3,4,5,6], &(rem(&1, 2) == 0)) # [2, 4, 6]
# Reduce
Enum.reduce([1, 2, 3], 0, fn x, acc -> x + acc end) # 6
Enum.reduce([1, 2, 3, 4], %{}, fn x, map ->
Map.put(map, x, x * x)
end) # %{1 => 1, 2 => 4, 3 => 9, 4 => 16}
# Sort
Enum.sort([3, 1, 2]) # [1, 2, 3]
Enum.sort([3, 1, 2], :desc) # [3, 2, 1]
Enum.sort_by(["aa", "a", "bbb"], &String.length/1) # ["a", "aa", "bbb"]
Enum.sort_by([%{a: 2}, %{a: 1}], & &1.a) # [%{a: 1}, %{a: 2}]
# Search
Enum.find([1,2,3,4], &(&1 > 2)) # 3
Enum.find([1,2,3], &(&1 > 5)) # nil
Enum.find_index([1,2,3], &(&1 == 2)) # 1
Enum.member?([1,2,3], 2) # true
Enum.any?([1,2,3], &(&1 > 5)) # false
Enum.all?([1,2,3], &(&1 > 0)) # true
# Grouping
Enum.group_by([1,2,3,4,5,6], &(rem(&1, 2)))
# %{0 => [2, 4, 6], 1 => [1, 3, 5]}
Enum.chunk_every([1,2,3,4,5,6,7], 3) # [[1,2,3], [4,5,6], [7]]
Enum.uniq([1,2,2,3,3,3]) # [1, 2, 3]
Enum.zip([1,2,3], [:a,:b,:c]) # [{1,:a}, {2,:b}, {3,:c}]
# Aggregation
Enum.sum([1,2,3]) # 6
Enum.product([1,2,3]) # 6
Enum.min([3,1,2]) # 1
Enum.max([3,1,2]) # 3
Enum.count([1,2,3]) # 3
Enum.join(["a","b","c"], "-") # "a-b-c"
# Comprehensions
for x <- 1..5, x > 2, do: x * x # [9, 16, 25]
for {k, v} <- %{a: 1, b: 2}, do: {v, k} # [{1, :a}, {2, :b}]
for x <- 1..3, y <- 1..x, do: {x, y} # [{1,1}, {2,1}, {2,2}, {3,1}, {3,2}, {3,3}]| Enum | Stream |
|---|---|
| Eager evaluation | Lazy evaluation |
| Works on finite lists | Works on infinite sequences |
| Returns results immediately | Returns on demand |
| Uses memory proportional to data | Constant memory (per step) |
| Enum.map(list, f) | Stream.map(stream, f) |
| Function | Description |
|---|---|
| Stream.iterate/2 | Infinite stream from seed |
| Stream.unfold/2 | Generate from accumulator |
| Stream.cycle/1 | Repeat a collection forever |
| Stream.repeatedly/1 | Call fn repeatedly |
| Stream.resource/3 | Resource management |
| Stream.run/1 | Execute lazy computation |
| Enum.to_list/1 | Materialize stream |
# ── Create a new project ──
mix new my_app # new Elixir project
mix new my_app --sup # with OTP supervisor
mix phx.new my_web # new Phoenix web app
# ── Common Mix Commands ──
mix deps # list dependencies
mix deps.get # fetch dependencies
mix deps.update dep_name # update a dependency
mix deps.tree # dependency tree
mix compile # compile project
mix run -e "IO.puts('hello')" # run one-liner
mix test # run tests
mix test test/my_test.exs:13 # run specific test line
mix test --only wip # run tests with @tag :wip
mix format # format code
mix format --check-formatted # check formatting
mix credo # linting
mix dialyzer # static type analysis
mix iex -S mix # interactive shell with project
# ── Project Structure ──
# my_app/
# lib/
# my_app.ex # main module
# my_app/
# user.ex # User module
# repo.ex # database repo
# test/
# my_app_test.exs # main test
# my_app/
# user_test.exs # User tests
# config/
# config.exs # base config
# dev.exs # dev overrides
# prod.exs # prod overrides
# test.exs # test overrides
# mix.exs # project & deps definition# ── mix.exs ──
defmodule MyApp.MixProject do
use Mix.Project
def project do
[
app: :my_app,
version: "0.1.0",
elixir: "~> 1.16",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
def application do
[
extra_applications: [:logger],
mod: {MyApp.Application, []} # OTP Application callback
]
end
defp deps do
[
{:phoenix, "~> 1.7"},
{:ecto_sql, "~> 3.10"},
{:jason, "~> 1.4"},
{:plug_cowboy, "~> 2.7"},
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
{:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false},
]
end
end# ── Spawn a process ──
pid = spawn(fn -> IO.puts("Hello from process") end)
pid = spawn(fn -> receive do
:ping -> IO.puts("pong")
end end)
send(pid, :ping)
# ── GenServer (OTP Behaviour) ──
defmodule Counter do
use GenServer
# Client API
def start_link(initial_value \\ 0) do
GenServer.start_link(__MODULE__, initial_value, name: __MODULE__)
end
def increment do
GenServer.cast(__MODULE__, :increment) # async
end
def decrement do
GenServer.cast(__MODULE__, :decrement)
end
def get do
GenServer.call(__MODULE__, :get) # sync
end
# Server Callbacks
@impl true
def init(initial_value) do
{:ok, initial_value}
end
@impl true
def handle_call(:get, _from, count) do
{:reply, count, count}
end
@impl true
def handle_cast(:increment, count) do
{:noreply, count + 1}
end
@impl true
def handle_cast(:decrement, count) do
{:noreply, count - 1}
end
end
# ── Supervisor ──
defmodule MyApp.Application do
use Application
@impl true
def start(_type, _args) do
children = [
{Counter, 0},
{MyApp.Repo, []},
{MyApp.Endpoint, []}
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
end
# ── Task (one-off async work) ──
Task.start(fn -> do_expensive_work() end)
result = Task.await(task, 5000) # 5 second timeout
# ── Agent (simple state) ──
{:ok, pid} = Agent.start_link(fn -> 0 end)
Agent.update(pid, &(&1 + 1))
Agent.get(pid, & &1) # => 1| Callback | Purpose |
|---|---|
| init/1 | Initialize state |
| handle_call/3 | Synchronous request |
| handle_cast/2 | Async request (no reply) |
| handle_info/2 | Handle messages (send/2) |
| terminate/2 | Cleanup on shutdown |
| code_change/3 | Hot code upgrade |
| Strategy | Behavior |
|---|---|
| one_for_one | Restart only the failed child |
| one_for_all | Restart all children if one fails |
| rest_for_one | Restart failed + started after it |
# ── Router (lib/my_app_web/router.ex) ──
defmodule MyAppWeb.Router do
use MyAppWeb, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
plug MyAppWeb.Auth
end
scope "/", MyAppWeb do
pipe_through :browser
get "/", PageController, :home
resources "/users", UserController, only: [:index, :show]
resources "/posts", PostController
end
scope "/api", MyAppWeb.API do
pipe_through :api
resources "/articles", ArticleController, except: [:new, :edit]
end
end
# ── Controller ──
defmodule MyAppWeb.UserController do
use MyAppWeb, :controller
def index(conn, _params) do
users = Accounts.list_users()
json(conn, %{data: users})
end
def show(conn, %{"id" => id}) do
user = Accounts.get_user!(id)
json(conn, %{data: user})
end
def create(conn, %{"user" => user_params}) do
case Accounts.create_user(user_params) do
{:ok, user} ->
conn
|> put_status(:created)
|> json(%{data: user})
{:error, changeset} ->
conn
|> put_status(:unprocessable_entity)
|> json(%{errors: format_errors(changeset)})
end
end
end
# ── Ecto Schema ──
defmodule MyApp.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :name, :string
field :email, :string
field :age, :integer, default: 0
has_many :posts, MyApp.Blog.Post
timestamps()
end
@doc "Changeset for creating/updating user"
def changeset(user, attrs) do
user
|> cast(attrs, [:name, :email, :age])
|> validate_required([:name, :email])
|> validate_format(:email, ~r/^[^\s@]+@[^\s@]+\.[^\s@]+$/)
|> validate_length(:name, min: 2, max: 100)
|> unique_constraint(:email)
end
end# ── Unit Test (test/my_app_test.exs) ──
defmodule MyAppTest do
use ExUnit.Case, async: true
describe "Math.add/2" do
test "adds two positive numbers" do
assert Math.add(1, 2) == 3
end
test "handles negative numbers" do
assert Math.add(-1, -2) == -3
end
test "handles zero" do
assert Math.add(0, 0) == 0
refute Math.add(0, 1) == 0
end
end
describe "Math.factorial/1" do
test "0! = 1" do
assert Math.factorial(0) == 1
end
test "5! = 120" do
assert Math.factorial(5) == 120
end
test "raises on negative input" do
assert_raise FunctionClauseError, fn ->
Math.factorial(-1)
end
end
end
end
# ── Controller Test ──
defmodule MyAppWeb.UserControllerTest do
use MyAppWeb.ConnCase, async: true
test "index returns list of users", %{conn: conn} do
conn = get(conn, ~p"/api/users")
assert json_response(conn, 200)["data"] |> length() > 0
end
test "show returns a single user", %{conn: conn} do
user = insert(:user)
conn = get(conn, ~p"/api/users/#{user.id}")
assert json_response(conn, 200)["data"]["id"] == user.id
end
test "create with valid params", %{conn: conn} do
conn = post(conn, ~p"/api/users", %{user: %{name: "Alice", email: "a@b.com"}})
assert %{"id" => id} = json_response(conn, 201)["data"]
end
test "create with invalid params returns errors", %{conn: conn} do
conn = post(conn, ~p"/api/users", %{user: %{name: ""}})
assert json_response(conn, 422)["errors"] != %{}
end
end
# ── Setup & Fixtures ──
defmodule MyApp.DataCase do
use ExUnit.CaseTemplate
using do
quote do
alias MyApp.Repo
import Ecto
import Ecto.Changeset
import Ecto.Query
import MyApp.DataCase
end
end
setup tags do
MyApp.DataCase.setup_sandbox(tags)
:ok
end
def setup_sandbox(tags) do
pid = Ecto.Adapters.SQL.Sandbox.start_owner!(MyApp.Repo, shared: not tags[:async])
on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
end
def errors_on(changeset) do
Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
Regex.replace(~r"%{\w+}", msg, fn _, key ->
opts |> Keyword.get(key, key) |> to_string()
end)
end)
end
end