# The Elixir Cheatsheet (v1.20.4)

> Dense single-page reference for Elixir v1.20.4 (released 2026-08-28).
> Practice with [exlings](https://github.com/zoedsoupe/exlings) exercises.
> Inspired by [cheats.rs](https://cheats.rs/).
> Published by [LambdaStratum](https://lambdastratum.com/) · [elixir-cheatsheet.lambdastratum.com](https://elixir-cheatsheet.lambdastratum.com/)

---

# Getting Started

*Install the latest Elixir, run your first lines in IEx, and scaffold a project with Mix. Everything on this page targets Elixir v1.20.4.*

---

## Install Elixir (latest stable)
`exlings exercises: 001–003`

Elixir v1.20.4 requires Erlang/OTP 27+ and is compatible with OTP 29. Prefer a version manager — asdf or mise — so you can pin per project.

**Install via asdf (recommended)**

```sh
asdf plugin add erlang https://github.com/asdf-vm/asdf-erlang.git
asdf plugin add elixir https://github.com/asdf-vm/asdf-elixir.git
asdf install erlang latest
asdf install elixir latest
asdf global elixir latest

elixir --version
# Erlang/OTP 28 [erts-...] ...
# Elixir 1.20.4 (compiled with Erlang/OTP 28)
```

- macOS: brew install elixir
- Ubuntu/Debian: sudo apt install elixir (often older — prefer asdf/mise)
- Windows: installer from elixir-lang.org/install or winget install ElixirLang.Elixir
- Verify: elixir --version prints both OTP and Elixir versions

> [!WARNING]
> Elixir compiles to BEAM bytecode and depends on the Erlang runtime. If elixir --version fails, fix Erlang/OTP first — most "Elixir install problems" are OTP problems.

---

## IEx — the interactive shell
`since v1.20 · exlings exercises: 001–003`

REPL with autocomplete, history, breakpoints and helpers. The fastest way to poke at the standard library.

**Everyday IEx**

```sh
iex
iex> 1 + 1
2
iex> h String.split          # documentation for a function
iex> String.split("a,b", ",")
["a", "b"]
iex> source(String.split)    # print/open source location (since v1.20)
iex> i 1_000_000             # introspect any value's type
iex> v 2                     # recall result of line 2
iex> h()                     # IEx help — lists all helpers
```

**Run a project inside IEx**

```sh
iex -S mix          # start IEx with your Mix project compiled + loaded
# edit code, then:
iex> recompile()    # reload changed modules without leaving the shell
```

> iex --dbg prye lets dbg/1 calls drop you into an interactive pry shell.

---

## Hello, World — scripts vs projects
`exlings exercises: 001–003`

.exs scripts run top-to-bottom with elixir; real projects use mix new and compiled .ex sources.

**hello.exs — a script**

```elixir
# Comments start with #
IO.puts("Hello, World!")
name = "Elixir"
IO.puts("Hello, #{name}!")   # interpolation with #{…}

person = %{name: "Ada", lang: :elixir}
IO.inspect(person, label: "person")
```

> Run with elixir hello.exs. .exs = scripted/evaluated each run; .ex = compiled to .beam.

**A real Mix project**

```sh
mix new hello --sup          # --sup generates a supervision tree
cd hello
mix test                     # the scaffold ships with a passing test
mix run -e 'IO.puts("hi")'   # run one expression
MIX_OS_DEPS_COMPILE_PARTITION_COUNT=4 mix deps.compile  # parallel deps (1.19+)
```

> mix format formats the whole project; run it before every commit — it is the community standard.

**What a generated project looks like**

```elixir
defmodule Hello.MixProject do
  use Mix.Project

  def project do
    [
      app: :hello,
      version: "0.1.0",
      elixir: "~> 1.20",
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end

  def application do
    [extra_applications: [:logger], mod: {Hello.Application, []}]
  end

  defp deps, do: []
end
```

---

## The whole language in one view
`exlings exercises: 001–050`

How the pieces fit: Kernel is auto-imported, everything else is a module; data is immutable; functions live in modules; processes do the concurrency.

| Layer | What it is | Examples |
| --- | --- | --- |
| Kernel & special forms | auto-imported basics: def, if, case, +, |> | defmodule, =, for |
| Standard library | modules of pure functions over immutable data | Enum, String, Map, Integer |
| Data types | immutable values | :atoms, ‘strings’, [lists], %{maps}, {tuples}, <<binaries>> |
| Mix | build tool & task runner | mix new, mix test, mix format |
| OTP | processes, supervisors, behaviours | GenServer, Supervisor, Task, Agent |
| BEAM | the VM: pre-emptive schedulers, per-process heaps, GC | distribution, hot reload |

- Everything is an expression — if, case, even defmodule return values.
- Data is immutable — "modifying" a map returns a new map.
- Pattern matching is everywhere — =, function clauses, case, for, binary syntax.
- Errors are values first — {:ok, result} / {:error, reason} tuples; raise is for bugs.
- The compiler is your reviewer — v1.20 gradually type checks every program and warns about verified bugs for free.

---

# Basics

*Modules, immutability, operators and strings — the everyday grammar of Elixir. Immutability comes early because it changes how you write every function.*

---

## Modules & functions
`exlings exercises: 009–013`

All code lives in modules: defmodule opens one, def defines a public function, defp a private one, and the last expression is the return value.

**defmodule, def, defp, module attributes**

```elixir
defmodule Greet do
  @moduledoc "Docs for the whole module."
  @punct "!"                # module attribute = compile-time constant

  @doc "Greets a person by name."
  def greet(name, lang \\ :en) do
    salutation(lang) <> String.capitalize(name) <> @punct
  end

  defp salutation(:en), do: "Hello, "
  defp salutation(:pt), do: "Ola, "
end

Greet.greet("ada")          #=> "Hello, Ada!"
Greet.greet("ada", :pt)     #=> "Ola, Ada!"

IO.inspect(Greet.greet("ada"), label: "greet/1")
IO.inspect(Greet.greet("ada", :pt), label: "greet/2")
```

> There is no return — the last expression of a clause is its result, and functions are always called as Module.function(args).

- alias MyApp.Web.Router — afterwards call it as just Router; your default tool for referencing other modules
- require Logger — compile in a module's macros before using them; needed before Logger.debug/1
- import Integer, only: [is_even: 1] — pull functions/macros into local scope; use sparingly, explicit calls read better
- __MODULE__ — the current module as an atom: used in child specs, docs, and submodule names like defmodule __MODULE__.Sub
- @attr "value" — compile-time constants and tags; @moduledoc, @doc and @spec are just well-known attributes

---

## Immutability & rebinding
`exlings exercises: 001–003`

Data never mutates: functions return new values, and = only rebinds a name to a different value — every existing value stays intact.

**New values, not mutations**

```elixir
map1 = %{name: "Ada", role: :admin}

map2 = Map.put(map1, :role, :user)   # returns a NEW map

map1
#=> %{name: "Ada", role: :admin}     # map1 is untouched
map2
#=> %{name: "Ada", role: :user}

# Idiomatic style: rebind the same name once the old
# value has no further use
user = %{name: "Ada"}
user = Map.put(user, :active, true)

IO.inspect(map1, label: "map1 (untouched)")
IO.inspect(map2, label: "map2 (new)")
IO.inspect(user, label: "rebound")
```

- {:ok, value} / {:error, reason} — prefer matching: {:ok, content} = File.read(path)
- Do not care about the value? Use _: {:ok, _} = File.close(fd)
- Same, but self-documenting: _content — still binds, and suppresses the unused-variable warning
- Strings are immutable too — String.upcase(s) cannot change s in place

> [!WARNING]
> Coming from OOP there is no map.role = :user assignment: Map.put/3 returns a new map that you must capture (user = Map.put(user, ...)). Forgetting to capture the result is the classic beginner bug — the value you computed is silently discarded. The upside: passing data to a function can never change your copy, and structurally-shared updates stay cheap.

---

## Operators
`exlings exercises: 004–008`

Arithmetic, list and string concatenation, membership — and the difference between value equality == and strict equality ===.

**Everyday operators**

```elixir
iex> 1 + 2 * 3
7
iex> 10 / 4              # / always returns a float
2.5
iex> div(10, 4)          # truncated integer division (rem/2 = remainder)
2
iex> [1, 2] ++ [3, 4]    # list concatenation...
[1, 2, 3, 4]
iex> [1, 2, 3, 4] -- [2, 4]   # ...and difference
[1, 3]
iex> "foo" <> "bar"      # binary (string) concatenation
"foobar"
iex> :elixir in [:erlang, :elixir]   # membership
true
iex> 5 in 1..10          # works on ranges too
true

IO.puts(1 + 2 * 3)
IO.puts(10 / 4)
IO.puts(div(10, 4))
IO.inspect([1, 2] ++ [3, 4])
IO.inspect([1, 2, 3, 4] -- [2, 4])
IO.puts("foo" <> "bar")
IO.inspect(:elixir in [:erlang, :elixir])
IO.inspect(5 in 1..10)
```

**== — value equality**

```elixir
iex> 1 == 1.0
true
iex> "a" == "a"
true
iex> :ok == :ok
true
```

**=== — value AND type**

```elixir
iex> 1 === 1.0
false
iex> "a" === "a"
true
iex> :ok === :ok
true
```

> == compares values across the int/float boundary; === also requires the same type. Prefer === where the distinction matters (list indexes, map keys).

Comparisons < > <= >= follow a total order across all types: number < atom < reference < function < port < pid < tuple < map < list < binary — handy for sorting mixed data, surprising if you forget it.

---

## and/or vs &&/||

and/or/not demand a real boolean on the left and are allowed in guards; &&/||/! accept anything and treat only nil and false as falsy.

**Strict — and / or / not**

```elixir
iex> true and :ok
:ok
iex> false or :ok
:ok
iex> not false
true
iex> nil and :ok
** (BadBooleanError) expected a boolean on left-side of "and", got: nil
```

**Truthy — && / || / !**

```elixir
iex> 1 && "then"         # any non-false/nil is truthy
"then"
iex> nil || "else"
"else"
iex> !nil
true
iex> [1] || :never
[1]
```

> Only and/or/not compile inside when guards — &&/||/! are rejected there. Both flavours short-circuit.

- a || b — first truthy value, else b: the default-value idiom opts[:name] || "anon"
- a && b — b only if a is truthy, else a
- and/or require a boolean on the left and the VM enforces it with BadBooleanError
- not x needs a real boolean; !x accepts anything — same result, different strictness

---

## Strings quick start
`exlings exercises: 044–047`

Double-quoted UTF-8 binaries: interpolate with #{}, concatenate with <>, go multi-line with triple-quote heredocs.

**Interpolation, concatenation, heredocs**

```elixir
name = "Ada"

"Hello, #{name}!"        # interpolation — runs String.Chars.to_string/1
"Hello, " <> name        # concatenation (binaries only)

sql = """
  SELECT *
  FROM users
  """                    # heredoc: multi-line, indentation trimmed

String.upcase("elixir")  #=> "ELIXIR"
String.length("héllo")   #=> 5   (graphemes)
byte_size("héllo")       #=> 6   (bytes: é is 2 in UTF-8)

IO.inspect("Hello, #{name}!", label: "interpolated")
IO.inspect(String.upcase("elixir"), label: "upcase")
IO.inspect(String.length("héllo"), label: "graphemes")
IO.inspect(byte_size("héllo"), label: "bytes")
```

> [!WARNING]
> #{} needs a String.Chars implementation: numbers, atoms and strings are fine, but %{a: 1} or {1, 2} raise Protocol.UndefinedError — wrap them: #{inspect(%{a: 1})}. And interpolation only exists inside double quotes and lowercase sigils; single quotes are charlists (see Primitive Types).

---

# Primitive Types

*Numbers, atoms, booleans, binaries vs charlists, sigils and dates — the primitive data types and the gotchas each one hides.*

---

## Numbers
`exlings exercises: 004–008`

Integers and floats with underscores, base prefixes and codepoint syntax; / always floats — use div/2 and rem/2 for integer math.

**Literals**

```elixir
iex> 1_000_000             # underscores for readability
1000000
iex> 0b1010                # base 2
10
iex> 0o777                 # base 8
511
iex> 0xFF                  # base 16
255
iex> ?a                    # codepoint of a character
97
iex> 10 / 4                # / is float division
2.5
iex> div(10, 4)            # truncated integer division
2
iex> rem(-10, 3)           # remainder keeps the dividend's sign
-1

IO.inspect([1_000_000, 0b1010, 0o777, 0xFF, ?a])
IO.inspect([10 / 4, div(10, 4), rem(-10, 3)])
```

**Parsing, rounding, bit tricks**

```elixir
iex> Integer.ceil_div(7, 2)     # division rounded up (since v1.20)
4
iex> Integer.popcount(255)      # count of set bits (since v1.20)
8
iex> Float.round(3.14159, 2)
3.14
iex> Integer.parse("42 steps")  # leading integer + leftover
{42, " steps"}
iex> String.to_integer("42")
42
iex> String.to_float("2.5")     # requires a dot — "2" raises
2.5

IO.inspect([Integer.ceil_div(7, 2), Integer.popcount(255), Float.round(3.14159, 2)])
IO.inspect([Integer.parse("42 steps"), String.to_integer("42"), String.to_float("2.5")])
```

> div/2 and rem/2 are auto-imported from Kernel; Integer.ceil_div/2 and Integer.popcount/1 are new in v1.20.

---

## Atoms

Constants whose name is their value — :ok, :error, and every module name. Great as tags, dangerous when created from user input.

**Atoms are name-is-the-value constants**

```elixir
iex> :ok                      # an atom's name is its value
:ok
iex> :ok == :error
false
iex> String == :"Elixir.String"   # module aliases are atoms too
true
iex> to_string(:hello)            # atoms convert to strings
"hello"
iex> :"with spaces"               # quoted atoms
:"with spaces"

IO.inspect([:ok == :error, String == :"Elixir.String", to_string(:hello)])
```

**Creating atoms from strings**

```elixir
iex> String.to_atom("user_" <> "1")       # creates a NEW atom
:"user_1"
iex> String.to_existing_atom("user_1")    # only if it already exists
:"user_1"

# The one call that cannot fail politely has to be rescued:
try do
  String.to_existing_atom("never_created_123")
rescue
  e in ArgumentError -> IO.puts("rescued: #{Exception.message(e)}")
end
```

> [!WARNING]
> The atom table only grows until the VM restarts. Turning untrusted input into atoms with String.to_atom/1 is a denial-of-service vector — an attacker can exhaust memory one request at a time. Use String.to_existing_atom/1 for anything that did not originate in your own code.

---

## Booleans & nil

true, false and nil are atoms with special status; only nil and false are falsy — everything else is truthy.

**Truthiness in practice**

```elixir
iex> true and is_atom(:ok)          # and/or demand booleans
true
iex> nil || false || 0 || "default" # only nil and false are falsy
0
iex> nil && :never_evaluated        # &&/|| accept any value
nil
iex> is_nil(nil)
true
iex> not is_nil(%{})                # there is no nil?/1 in Elixir 1.20
false

IO.inspect([
  true and is_atom(:ok),
  nil || false || 0 || "default",
  nil && :never_evaluated,
  is_nil(nil),
  not is_nil(%{}),
])

try do
  nil and :never_evaluated
rescue
  e -> IO.puts("and demands booleans: " <> Exception.message(e))
end
```

> true, false and nil are atoms under the hood (true == :true), but specs treat them as the distinct types boolean() and nil.

- value || default — fallback for nil/false
- && and || accept any value on the left; and and or raise BadBooleanError on anything that is not true/false
- Map.get(map, :key, default) — default for missing keys, not just nil values
- Map.fetch/2 returns {:ok, v} or :error — distinguishes missing from stored-nil (see Collections)
- is_nil/1 is guard-safe and idiomatic; avoid == nil in when clauses
- and/or/not/in/is_nil all work in guards — there is no nil?/1 in Elixir 1.20

---

## Strings vs charlists vs binaries

"hello" is a UTF-8 binary, 'hello' is a charlist (list of codepoints) — and <<...>> binaries are the primitive underneath it all.

**String — UTF-8 binary**

```elixir
iex> s = "hello"
"hello"
iex> is_binary(s)
true
iex> s <> "!"               # binaries concatenate with <>
"hello!"
iex> ?h                     # codepoint syntax
104
iex> ?a == 97
true
```

**Charlist — list of codepoints**

```elixir
iex> c = ~c"hello"          # identical to 'hello'
'hello'
iex> is_list(c)
true
iex> c ++ [33]              # lists concatenate with ++
'hello!'
iex> hd(c)                  # each element is a codepoint
104
```

> 'hello' and ~c(hello) are the same charlist. Charlists appear at the Erlang edges — some :crypto/:code returns, module-name arguments — and rarely anywhere else. Strings are what you use 99% of the time; <<...>> binaries are the primitive Elixir builds them from.

> [!WARNING]
> A list of small integers prints as a charlist in iex — ~c(hi) instead of [104, 105]. is_list/1 is true for charlists and ordinary lists alike: use is_binary/1 to recognize strings, and List.to_string/1 to convert a charlist.

---

## Sigils overview
`exlings exercises: 071–073`

Sigils are literal-syntax shorthands: ~s strings, ~c charlists, ~w word lists, ~r regexes, ~D ~T ~N ~U dates — uppercase means fully literal.

**The common sigils**

```elixir
iex> ~s(a #{1 + 1} b)          # ~s: string, interpolates
"a 2 b"
iex> ~S(a #{1 + 1} b)          # uppercase: fully literal
"a \#{1 + 1} b"
iex> ~w(see you soon)a         # words; modifier a/s/c = atoms/strings/charlists
[:see, :you, :soon]
iex> ~r/ab+c/                  # regex — see the Regex module
~r/ab+c/
iex> ~c(a charlist)
'charlist'

IO.inspect([~s(a #{1 + 1} b), ~S(a #{1 + 1} b), ~w(see you soon)a, ~c(a charlist)])
```

- Delimiters — any sigil accepts (), [], {}, <>, // or quote pairs; pick the one that needs no escaping inside
- Heredocs — sigils combine with """ for multi-line text, keeping interpolation rules
- Date sigils — ~D ~T ~N ~U build date/time structs (next entry)
- Uppercase = literal — ~S skips both interpolation and escape processing

> [!WARNING]
> Lowercase sigils process #{} interpolation and escape sequences (\n becomes a newline); uppercase sigils keep every character verbatim. Grabbing the wrong case is a classic bug when a literal \n-style sequence is what you actually wanted to type.

---

## Dates & time

Four sigils build the date/time structs; Date.shift/3 moves whole calendar units, and to_timeout/1 turns duration lists into milliseconds.

**Date, Time, NaiveDateTime, DateTime**

```elixir
iex> date = ~D[2026-06-03]            # Date struct (no time, no zone)
iex> time = ~T[08:30:00.500]          # Time struct
iex> naive = ~N[2026-06-03T08:30:00]  # NaiveDateTime (no zone)
iex> dt = ~U[2026-06-03T08:30:00Z]    # DateTime, always UTC
iex> Date.shift(date, month: 1)       # calendar-safe shift (since v1.17)
~D[2026-07-03]
iex> DateTime.compare(dt, ~U[2026-12-01T00:00:00Z])
:lt
iex> to_timeout([minute: 5])          # duration -> milliseconds
300000

IO.inspect([
  Date.shift(~D[2026-06-03], month: 1),
  DateTime.compare(~U[2026-06-03T08:30:00Z], ~U[2026-12-01T00:00:00Z]),
  to_timeout([minute: 5]),
])
```

> DateTime.compare/2 returns :lt | :eq | :gt; to_timeout/1 feeds Process.sleep/1 and timeouts.

- Date.shift/3 moves whole units and lands on real dates — Jan 31 plus one month is Feb 28; Date.add/2 just adds days
- DateTime.diff/2 returns seconds between two DateTimes; compare when order is all you need
- ~N has no timezone — convert with DateTime.from_naive/2 before comparing against a ~U
- Comparisons need the same kind — Date.compare/2 for dates, DateTime.compare/2 for zoned datetimes; mixing raises

---

# Collections

*Lists, tuples, keyword lists, maps, structs and ranges — what each one is good at, and a table for choosing between them.*

---

## Lists
`exlings exercises: 023–027`

Singly-linked lists: prepending is O(1), everything else walks the list — so build by prepending and access through Enum.

**Head, tail, concat, access**

```elixir
iex> list = [3, 1, 2]
iex> [head | tail] = list      # head = first element, tail = rest
iex> {head, tail}
{3, [1, 2]}
iex> hd(list)                  # first element (tl/1 = rest)
3
iex> [0 | list]                # prepend: O(1)
[0, 3, 1, 2]
iex> list ++ [4]               # append: O(n) — walks every node
[3, 1, 2, 4]
iex> [1, 2, 3, 4] -- [2, 4]    # difference
[1, 3]
iex> length(list)              # O(n) — counts node by node
3
iex> Enum.at(list, 2)          # O(n) — random access walks the list
2

IO.inspect({head, tail})
IO.inspect([hd(list), length(list), Enum.at(list, 2)])
IO.inspect([0 | list])
IO.inspect(list ++ [4])
IO.inspect([1, 2, 3, 4] -- [2, 4])
```

**Prepend — O(1)**

```elixir
def record(event, log) do
  # new node points at the existing list
  [event | log]
end
```

**Append — O(n)**

```elixir
def record(event, log) do
  # must copy the whole list to reach the end
  log ++ [event]
end
```

> To keep chronological order, prepend while building and call Enum.reverse/1 once at the end — far cheaper than repeated appends.

Lists are linked cons cells, not arrays. Day-to-day list work goes through Enum (eager) and Stream (lazy); the List module covers the recursive, pattern-matching side.

---

## Tuples
`exlings exercises: 023–027`

Fixed-size, O(1)-access containers — the home of tagged return values like {:ok, value} and {:error, reason}.

**elem, put_elem, tuple_size**

```elixir
iex> tuple = {:ok, 42}
iex> elem(tuple, 0)
:ok
iex> elem(tuple, 1)
42
iex> put_elem(tuple, 1, 43)   # returns a NEW tuple
{:ok, 43}
iex> tuple_size(tuple)        # O(1) — size is stored, unlike length/1
2
iex> {:error, reason} = {:error, :enoent}
iex> reason
:enoent

IO.inspect({elem(tuple, 0), elem(tuple, 1)})
IO.inspect(put_elem(tuple, 1, 43))
IO.inspect(tuple_size(tuple))
IO.inspect(reason)
```

- Fixed-size containers — adding or removing elements copies; growing data belongs in lists or maps
- {:ok, value} / {:error, reason} — the ecosystem-wide return convention; pattern match it, do not ignore it
- O(1) access — elem/2 and tuple_size/1 are constant-time, unlike list length/1
- File.read/1 returns {:ok, binary} | {:error, posix} — the tagged tuple in the wild

---

## Keyword lists
`exlings exercises: 023–027`

Ordered [key: value] lists of two-tuples: duplicate keys allowed, order preserved — the default for function options and DSLs.

**Sugar for tuples of atoms**

```elixir
iex> kw = [name: "Ada", role: :admin]
iex> kw == [{:name, "Ada"}, {:role, :admin}]   # sugar for tuples
true
iex> [name: "Ada", name: "Grace"]              # ordered, duplicates allowed
[name: "Ada", name: "Grace"]
iex> Keyword.get(kw, :missing, :default)
:default
iex> Keyword.fetch(kw, :role)
{:ok, :admin}
iex> Keyword.keys(kw)
[:name, :role]

IO.inspect(kw == [{:name, "Ada"}, {:role, :admin}])
IO.inspect([
  Keyword.get(kw, :missing, :default),
  Keyword.fetch(kw, :role),
  Keyword.keys(kw),
])
```

**The last-argument sugar**

```elixir
defmodule Client do
  def connect(host, opts \\ []) do
    port = Keyword.get(opts, :port, 4000)
    {host, port}
  end
end

IO.inspect(Client.connect("db.local"))                    # opts = []
IO.inspect(Client.connect("db.local", port: 5433, timeout: 5_000))
# a trailing bracket-less keyword list becomes the LAST argument
```

Keyword list or map? Keyword list when order matters, keys may repeat, or you are defining options/DSLs. Map for general key-value data with unique keys. Note only the first key of duplicates is visible via kw[:key] — use the Keyword module to see them all.

---

## Maps & MapSet
`exlings exercises: 023–027`

The general key-value store: %{} with atom-key sugar or => for any keys — plus MapSet for uniqueness and set algebra.

**Access patterns**

```elixir
iex> m = %{name: "Ada", role: :admin}    # atom keys: shorthand
iex> m.name                              # dot access: only for known atom keys
"Ada"
iex> m[:role]                            # bracket syntax: any key type
:admin
iex> mixed = %{"a" => 1, :b => 2, 3 => "c"}   # => syntax: any key type
iex> Map.fetch!(m, :name)                # raises KeyError when missing
"Ada"
iex> Map.get(m, :missing, "fallback")
"fallback"
iex> Map.fetch(m, :missing)              # :error instead of raising
:error

IO.inspect([m.name, m[:role], Map.fetch!(m, :name)])
IO.inspect([Map.get(m, :missing, "fallback"), Map.fetch(m, :missing)])
```

> Map.fetch/2 distinguishes missing (:error) from stored-nil; Map.get/3 and bracket access collapse both into nil/default.

**MapSet — unique members**

```elixir
iex> set = MapSet.new([1, 2, 2, 3])   # duplicates collapse
iex> MapSet.member?(set, 2)
true
iex> MapSet.union(set, MapSet.new([3, 4]))
#MapSet<[1, 2, 3, 4]>
iex> MapSet.intersection(set, MapSet.new([2, 3, 9]))
#MapSet<[2, 3]>

IO.inspect(MapSet.member?(set, 2))
IO.inspect(MapSet.union(set, MapSet.new([3, 4])))
IO.inspect(MapSet.intersection(set, MapSet.new([2, 3, 9])))
```

> [!WARNING]
> m.field works only for atom keys and raises KeyError otherwise; for string or dynamic keys use Map.get/3, Map.fetch/2 or the bracket syntax m[key]. Since v1.20 the compiler also tracks each map's possible key domain — including what Map.put/3, Map.delete/2 and Map.replace/3 do to it — and warns when you read a key that cannot exist.

---

## Structs
`exlings exercises: 023–027`

Maps with a module attached: defstruct fields with defaults, optional compile-time enforcement, and checked update syntax.

**defstruct, enforce_keys, updates**

```elixir
defmodule User do
  @enforce_keys [:name]
  defstruct name: nil, role: :member, tags: []
end

defmodule Demo do
  def run do
    user = %User{name: "Ada"}          # missing :name = compile error
    admin = %User{user | role: :admin} # update syntax: existing fields only
    IO.inspect(user.role)
    IO.inspect(admin)
  end
end

Demo.run()
# %User{user | nope: 1}              # unknown key fails to compile (v1.18+)
```

> %User{user | ...} is the update syntax — it requires an existing struct and only accepts declared fields, verified by the compiler since v1.18.

**Banned since v1.19**

```elixir
defmodule Filter do
  # compile error — regexps are not allowed
  # as struct defaults
  defstruct pattern: ~r/foo/i
end
```

**Compile in a constructor**

```elixir
defmodule Filter do
  defstruct pattern: nil

  def new(source) do
    %Filter{pattern: Regex.compile!(source)}
  end
end
```

> A struct default must be a literal value; regexps hold runtime resources, so compile them inside a constructor function like new/1.

- @derive Jason.Encoder — generate a protocol implementation for the struct instead of writing it by hand
- @enforce_keys — make required fields fail at construction time, not deep in runtime code
- Structs are bare maps — is_map(user) is true; they just add a __struct__ key and compile-time field checking
- Defaults are compile-time constants — reach for a new/1 function when fields need computing

---

## Ranges — and which collection when?

first..last//step sequences are enumerables — and the fastest way to answer the eternal question: which collection do I reach for?

**Ranges**

```elixir
iex> range = 1..10              # inclusive, ascending
iex> 5 in range
true
iex> Enum.map(1..3, &(&1 * 10))
[10, 20, 30]
iex> Enum.sum(1..100//2)        # step: 1, 3, 5, ..., 99
2500
iex> Enum.to_list(10..1//-1)    # descending: explicit negative step
[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
iex> Range.disjoint?(1..5, 6..10)
true
iex> Range.disjoint?(1..5, 5..10)
false

IO.inspect([
  5 in range,
  Enum.map(1..3, &(&1 * 10)),
  Enum.sum(1..100//2),
  Range.disjoint?(1..5, 6..10),
  Range.disjoint?(1..5, 5..10),
])
IO.inspect(Enum.to_list(10..1//-1))
```

| Collection | Ordered | Duplicates | Access | Typical use |
| --- | --- | --- | --- | --- |
| List | yes | yes | [h | t], Enum.at/2 — O(n) | iteration, variable-length data |
| Tuple | yes | yes | elem/2 — O(1) | fixed-size records, tagged returns |
| Keyword list | yes | yes (keys) | kw[:key], Keyword.get/3 | function options, DSLs |
| Map | no | keys unique | map.key, Map.fetch!/2 | general key-value data |
| Struct | no | keys fixed | struct.field | typed records, module data |
| MapSet | no | no | MapSet.member?/2 | uniqueness, set algebra |

---

# Pattern Matching

*The match operator, destructuring, the pin operator, function clauses and binary patterns — the single most-used feature of the language.*

---

## The match operator
`exlings exercises: 014–018`

= is not assignment: the left side is a pattern matched against the right — it binds variables or raises MatchError.

**Patterns bind — or fail loudly**

```elixir
iex> {a, b} = {1, 2}          # tuples destructure element-wise
iex> a + b
3
iex> [x, y, z] = [1, 2, 3]    # lists destructure positionally
iex> x + y + z
6
iex> {x, x} = {1, 1}          # repeated vars must be equal
iex> %{age: age} = %{age: 30}   # partial match: take the keys you need

IO.inspect(a + b)
IO.inspect(x + y + z)
IO.inspect(age)

# A repeated variable is a match condition, not a rebind:
try do
  {x, x} = {1, 2}
rescue
  e -> IO.puts("rescued: " <> Exception.message(e))
end
```

= is the match operator: Elixir tries to make both sides equal. Fresh variables bind; already-bound variables must match the right side exactly — otherwise the whole expression raises MatchError.

> [!WARNING]
> %{age: age, age: age} = %{age: 30} looks like a way to assert a key appears twice, and it is a compile error in Elixir 1.20: *key :age will be overridden in map*. Map patterns are partial, so there is nothing for the duplicate to prove. Repeated variables ({x, x}) are a different feature and do work — they are a match condition.

---

## Destructuring
`exlings exercises: 014–018`

Pull values out of lists, maps, tuples and any nesting — but remember map patterns are partial: extra keys are ignored.

**Lists, maps, nesting**

```elixir
iex> [h | t] = [1, 2, 3]
iex> {h, t}
{1, [2, 3]}
iex> %{name: name} = %{name: "Ada", age: 36}   # partial match!
iex> name
"Ada"
iex> %{user: %{name: name}} = %{user: %{name: "Grace", admin: true}}
iex> name
"Grace"
iex> {:ok, [first | _rest]} = {:ok, [1, 2, 3]}
iex> first
1

IO.inspect({h, t})
IO.inspect(name)   # rebound by the nested match above
IO.inspect(first)
```

- Head/tail — [h | t] splits any non-empty list; [h | _] just asserts it is non-empty
- Nested to any depth — %{user: %{address: %{city: city}}}
- Containers compose — {:ok, [first | _]} matches an ok-tuple wrapping a list
- String prefixes — "Bearer " <> token splits off a prefix (binary patterns get their own entry)

> [!WARNING]
> %{name: name} matches any map that contains a :name key — extra keys are silently ignored. That is a feature for grabbing fields from bigger payloads, and a bug when you meant to require the exact shape. Enforce exactness with map_size/1 in a guard or by matching every field.

---

## The pin operator
`exlings exercises: 014–018`

^var matches against an existing binding instead of creating a new one; _ never binds, _var binds quietly.

**Pinning in matches**

```elixir
iex> expected = 2
iex> {^expected, actual} = {2, 9}     # ^ reads the existing binding
iex> {expected, actual}
{2, 9}

IO.inspect({expected, actual})

# A pin that does not match is a MatchError, not a rebind:
try do
  {^expected, _} = {3, 4}
rescue
  e -> IO.puts("rescued: " <> Exception.message(e))
end
```

**Wrong — silently rebinds**

```elixir
status = :pending

# no pin: this creates a NEW binding
{status, other} = {:done, 1}

status
#=> :done  — :pending was lost
```

**Right — pin with ^**

```elixir
status = :pending

# ^ reads the EXISTING binding
{^status, other} = {:done, 1}
#=> ** (MatchError) — :pending does not match :done
```

> _ matches anything and never binds — repeat it freely in one pattern. _var does bind; it only silences the unused-variable warning, so reading it later is a smell.

---

## Function clauses & case
`exlings exercises: 014–018`

Multiple clauses try top to bottom and the first match wins — so order specific patterns before general ones, and use when guards to refine.

**Multi-clause functions and case**

```elixir
defmodule Math do
  def sign(n) when n > 0, do: :positive    # guard refines the clause
  def sign(0), do: :zero                   # literal pattern
  def sign(n) when n < 0, do: :negative
  def sign(_), do: {:error, :not_a_number} # catch-all last
end

defmodule Demo do
  def run do
    IO.inspect([Math.sign(3), Math.sign(0), Math.sign(-2), Math.sign("x")])

    case File.read("mix.exs") do
      {:ok, content} -> {:read, byte_size(content)}
      {:error, :enoent} -> :missing
      {:error, reason} -> {:failed, reason}    # cover every shape
    end
    |> IO.inspect(label: "case")
  end
end

Demo.run()
```

**Wrong — general clause first**

```elixir
def handle({:error, reason}), do: {:log, reason}
def handle({:error, :enoent}), do: :missing

# the second clause can NEVER match —
# the compiler even warns about it
```

**Right — specific first**

```elixir
def handle({:error, :enoent}), do: :missing
def handle({:error, reason}), do: {:log, reason}
def handle(:ok), do: :ok

# no matching clause at all ->
# FunctionClauseError at runtime
```

> Clauses try top to bottom — put specific patterns before general ones. A case that runs out of clauses raises CaseClauseError.

Guards extend patterns with boolean checks: when is_integer(n) and n > 0. Only guard-safe expressions are allowed — is_integer/1, map_size/1, comparisons, and/or/not — no &&/||, no custom functions, no side effects.

---

## Binary patterns
`exlings exercises: 014–018`

Match binaries byte-for-byte with <<...>>: fixed widths, ::binary tails, ::utf8 codepoints and string prefix patterns.

**Sizes, utf8, prefixes**

```elixir
iex> <<a, b>> = <<1, 2>>        # each segment defaults to 8 bits
iex> {a, b}
{1, 2}
iex> <<len, rest::binary>> = <<3, 97, 98, 99>>
iex> {len, rest}
{3, "abc"}
iex> <<a::8, b::16, c::8>> = <<1, 0, 5, 9>>   # bit-sized segments
iex> {a, b, c}
{1, 5, 9}
iex> <<ch::utf8>> = "é"         # one UTF-8 codepoint (2 bytes here)
iex> ch
233
iex> "Bearer " <> token = "Bearer abc123"     # prefix match on strings
iex> token
"abc123"
iex> <<w1::binary-size(2), _::binary>> = "elixir"
iex> w1
"el"

IO.inspect({a, b})
IO.inspect({len, rest})
IO.inspect(ch)
IO.inspect(token)
IO.inspect(w1)
```

**Parsing a binary file header**

```elixir
defmodule PNG do
  @magic <<137, 80, 78, 71, 13, 10, 26, 10>>   # PNG file signature

  def parse(<<@magic::binary, width::32, height::32, _rest::binary>>) do
    {:ok, width, height}
  end

  def parse(_other), do: {:error, :not_a_png}
end

PNG.parse(<<
  137, 80, 78, 71, 13, 10, 26, 10,   # signature
  0, 0, 8, 0,                        # width  = 2048
  0, 0, 4, 80                        # height = 1104
>>)
|> IO.inspect()   #=> {:ok, 2048, 1104}

PNG.parse("not a png")
|> IO.inspect(label: "no match")
```

> ::32 reads a big-endian 32-bit integer and @magic::binary splices the attribute's exact bytes into the pattern. Since v1.20 the type system tracks tuple and binary sizes through matches, so this pattern also proves the data layout to the compiler.

---

# Functions

*Named functions with pattern-matched clauses, guarded branches, anonymous functions and the capture operator — functions are values you can pass anywhere.*

---

## Named functions: def & defp
`exlings exercises: 009–013`

Functions live in modules and are defined as clauses that pattern match top-down — the first matching clause runs. Arity is part of the identity: Text.format/2.

A function is identified by module, name and arity (format/2). Several clauses sharing a name and arity form one function: Elixir tries them in order and runs the first whose pattern matches.

**Clauses, privacy, arity**

```elixir
defmodule Text do
  # public — callable from other modules as Text.format/2
  def format(lines, width) do
    lines
    |> Enum.join(" ")
    |> wrap(width)
  end

  # private — visible only inside this module
  defp wrap(string, width) when byte_size(string) <= width, do: string
  defp wrap(string, width), do: String.slice(string, 0, width) <> "…"
end

Text.format(["elixir", "rocks"], 40)
|> IO.inspect()   #=> "elixir rocks"
```

> format/2 and format/3 would be two different functions — adding a parameter changes the arity and therefore the identity.

- First match wins — order specific clauses before general ones
- Arity is part of the name — format/2 and format/3 are unrelated functions
- defp is module-private — calling it from another module is a compile error
- Guards and default arguments (next entries) compose with clauses

---

## Default arguments
`exlings exercises: 009–013`

The \\ operator (two backslashes) gives a parameter a default and generates one exported function per arity: join/1 falls back to join/2.

A defaulted parameter is filled when the caller omits it. Elixir compiles one clause into several arities, so defaults are visible to callers and docs.

**join/1 delegates to join/2**

```elixir
defmodule Join do
  def join(list, sep \\ ", "), do: Enum.join(list, sep)
end

Join.join(["a", "b", "c"])
|> IO.inspect(label: "join/1")

Join.join(["a", "b", "c"], " + ")
|> IO.inspect(label: "join/2")
```

> [!WARNING]
> With several clauses, declare a body-less head that carries the default, then write the clauses below it. Repeating \\ defaults on each clause is a compile error — Elixir must know exactly which parameter each default fills.

---

## Guards: when
`since v1.20 · exlings exercises: 014–018`

Guards are boolean checks after a pattern: when filters clauses, defguard composes your own, and since v1.20 they also feed the type checker.

Patterns match shapes; guards check properties. Guards are restricted to a safe set of expressions so they can never crash or side-effect — a failed guard just skips the clause.

**Built-in and custom guards**

```elixir
defmodule Access do
  # defguard composes guards into a reusable name
  defguard is_adult(age) when is_integer(age) and age >= 18

  def check(%{age: age}) when is_adult(age), do: :allowed
  def check(%{age: age}) when is_integer(age), do: :minor
  def check(_person), do: :unknown
end

Access.check(%{age: 21})   #=> :allowed
Access.check(%{age: 9})    #=> :minor
Access.check(%{})          #=> :unknown

IO.inspect([
  Access.check(%{age: 21}),
  Access.check(%{age: 9}),
  Access.check(%{}),
])
```

- Types: is_integer/1, is_float/1, is_binary/1, is_atom/1, is_boolean/1, is_nil/1
- Collections: is_list/1, is_map/1, is_tuple/1, is_map_key/2, key in map_or_list
- Logic: and, or, not, ==, !=, <, > — no arbitrary function calls allowed, except those defined by defguard
- Since v1.20 guards are type-inferred: when is_list(items) and is_integer(n) narrows the clause body's types, so the compiler checks the code inside for free

---

## Anonymous functions
`exlings exercises: 009–013`

fn args -> body end creates a function value, called with a dot. They support multiple clauses, guards, and close over variables by value.

Anonymous functions are first-class values — store them, pass them, return them. The call syntax is fun.(args): the dot makes the call explicit.

**fn, dot calls, closures**

```elixir
double = fn x -> x * 2 end
double.(4)          #=> 8 — call with a DOT

# multiple clauses, matched top-down, guards allowed
classify = fn
  x when x < 0 -> :negative
  0 -> :zero
  _x -> :positive
end
classify.(-5)       #=> :negative

# closures capture by value — data is immutable
n = 10
add_n = fn x -> x + n end
n = 0               # rebinding outside does not change the closure
add_n.(5)           #=> 15

IO.inspect([double.(4), classify.(-5), add_n.(5)])
```

> Forgetting the dot raises UndefinedFunctionError — double(4) would look for a named function double/1.

---

## The capture operator: &
`since v1.19 · exlings exercises: 009–013`

&Mod.fun/arity grabs a named function as a value; &(&1 + 1) writes tiny anonymous ones with &1 placeholders. Types propagate through captures since v1.19.

& is shorthand for a function value. Use it on an existing function (&Module.fun/arity — Erlang functions too) or build one inline with the &1, &2… placeholders.

**Four ways to capture**

```elixir
Enum.map([1, 2, 3], &(&1 * 10))       #=> [10, 20, 30]
add = &(&1 + &2)                      # &2 = second argument
add.(3, 4)                            #=> 7

Enum.reduce([1, 2, 3], 0, &+/2)       # capture operators too
Enum.map(["a", :b, 2], &to_string/1)  #=> ["a", "b", "2"]

upcase = &String.upcase/1             # named function as value
remote = &:erlang.node/0              # Erlang functions included
upcase.("elixir")                     #=> "ELIXIR"

IO.inspect([
  Enum.map([1, 2, 3], &(&1 * 10)),
  add.(3, 4),
  Enum.reduce([1, 2, 3], 0, &+/2),
  Enum.map(["a", :b, 2], &to_string/1),
  upcase.("elixir"),
])
```

> Since v1.19 the compiler propagates types through captures — Enum.map(names, &String.upcase/1) is fully type-checked end to end.

---

## Higher-order functions & then/2
`exlings exercises: 009–013`

Functions are values: take them as arguments, return them from factories, and use then/2 to keep a pipeline flowing when the value is not the first argument.

Any function that accepts or returns functions is higher-order — Enum.map/2, Enum.reduce/3, Kernel.then/2. When a pipeline step does not take the piped value first, or needs branching mid-pipe, then/2 inserts it cleanly.

**Functions as arguments and factories**

```elixir
transform = fn fun, x -> fun.(fun.(x)) end
transform.(&(&1 * 3), 2)          #=> 18

# then/2 threads a value through a single function
port =
  System.get_env("ELIXIR_CHEATS_PORT")   # unset -> nil
  |> then(fn
    nil -> "4000"
    port -> port
  end)
  |> String.to_integer()

# functions building functions — closures as factories
prefix = fn tag -> fn msg -> "[#{tag}] #{msg}" end end
log = prefix.("info")
log.("started")                   #=> "[info] started"

IO.inspect(port)
IO.inspect(log.("started"))
```

- then(value, fun) simply calls fun.(value) — ideal mid-pipe
- The & capture plus higher-order Enum functions cover most everyday composition
- Closures (not curry/apply) are the idiomatic way to build configurable functions

---

# Control Flow

*Everything is an expression. Pick the construct by the shape of the decision: a condition, a value's shape, a chain of matches, or a collection to walk.*

---

## if / else / unless / cond
`since v1.17 · exlings exercises: 019–022`

if handles one condition, cond tries many boolean branches — and both are expressions you can assign. unless is soft-deprecated since v1.17.

There are no statements — if, cond and even defmodule return values, so bind the result. Only nil and false are falsy; everything else (including 0 and the empty string) is truthy.

**Branching as an expression**

```elixir
n = 7

parity =
  if rem(n, 2) == 0 do
    "even"
  else
    "odd"
  end
#=> "odd"

grade =
  cond do
    n >= 90 -> :a
    n >= 80 -> :b
    n >= 70 -> :c
    true -> :f            # true acts as the catch-all
  end
#=> :f

IO.inspect([parity, grade])
```

**unless — soft-deprecated**

```elixir
# warns since v1.17
unless Enum.empty?(errors) do
  IO.puts("found problems!")
end
```

**if not — idiomatic**

```elixir
if not Enum.empty?(errors) do
  IO.puts("found problems!")
end
```

> unless emits a deprecation warning since v1.17; the migration is mechanical — unless x becomes if not x.

---

## case — match & dispatch
`since v1.20 · exlings exercises: 019–022`

Match one value against clauses top-down, with guards per clause. Since v1.20 the compiler narrows types across clauses — handle nil first and it is gone later.

case pattern-matches the value against each clause in order; a guard can refine any clause. It replaces whole chains of if/else when the decision is about a value's shape.

**Clauses + guards, first match wins**

```elixir
resp = {201, %{id: 42}}

outcome =
  case resp do
    {status, _body} when status in 200..299 -> {:ok, :created}
    {404, _body} -> {:error, :not_found}
    {status, _body} when status >= 500 -> {:error, :server}
    _other -> {:error, :unknown}     # catch-all keeps this total
  end
#=> {:ok, :created}

IO.inspect(outcome)
```

**Type narrowing across clauses (v1.20)**

```elixir
case System.get_env("ELIXIR_CHEATS_PORT") do   # unset -> nil
  nil -> 4000                      # nil is handled first...
  port -> String.to_integer(port)  # ...so port is a binary here
end
|> IO.inspect()
```

> Since v1.20 the compiler narrows across clauses: matching nil removes it from later clauses, so String.to_integer(port) is proven sound — no annotations needed.

> [!WARNING]
> An unmatched value raises CaseClauseError at runtime. Keep a catch-all _ -> clause for open-ended inputs — and since v1.20 the compiler warns when a clause can never match, so dead branches are not silent either.

---

## with — the happy path
`exlings exercises: 019–022`

Chain steps that each return {:ok, value}; <- pattern-matches and binds. The first non-matching step short-circuits — and returns its raw value.

with is a pipeline of pattern matches. <- takes the right side apart and binds variables; plain expressions run in order. Add else clauses to normalize failures.

**Chain matches, normalize failures with else**

```elixir
defmodule Profile do
  def city(map) do
    with {:ok, user} <- Map.fetch(map, :user),
         {:ok, address} <- Map.fetch(user, :address),
         {:ok, city} <- Map.fetch(address, :city) do
      {:ok, city}
    else
      :error -> {:error, :missing_key}   # Map.fetch fails with :error
      other -> other                     # any other miss, passed through
    end
  end
end

Profile.city(%{user: %{address: %{city: "Tokyo"}}})
#=> {:ok, "Tokyo"}

IO.inspect(Profile.city(%{user: %{address: %{city: "Tokyo"}}}))
IO.inspect(Profile.city(%{}))   # the else branch normalises the miss
```

> [!WARNING]
> If {:ok, x} <- expr receives :error or nil, nothing raises — the whole with returns that raw unmatched value. Callers expecting {:error, _} get a bare :error, or worse nil. Add an else to normalize, or make every right-hand side return a tagged tuple.

---

## for comprehensions
`since v1.19 · exlings exercises: 041–043`

Generate, filter and collect in one declarative pass over any Enumerable — with into: and reduce: to choose the destination. Generator types are checked since v1.19.

**Generators, filters, into:, reduce:**

```elixir
# generator + filter
for n <- 1..5, rem(n, 2) == 1, do: n * n
#=> [1, 9, 25]

# multiple generators nest like loops
for x <- [1, 2], y <- ~w(a b), do: {x, y}
#=> [{1, "a"}, {1, "b"}, {2, "a"}, {2, "b"}]

# pattern filters skip non-matching elements
for {:ok, value} <- [{:ok, 1}, :error, {:ok, 3}], do: value
#=> [1, 3]

# into: collects into any collectable — binaries, MapSets, tuples...
for s <- ["ab", "cd"], into: "", do: s
#=> "abcd"

for x <- [1, 2, 2, 3], into: MapSet.new(), do: x
#=> MapSet.new([1, 2, 3])

# reduce: turns the comprehension into an accumulation
for n <- 1..100, reduce: 0 do
  acc -> if rem(n, 3) == 0, do: acc + n, else: acc
end
#=> 1683

IO.inspect(for n <- 1..5, rem(n, 2) == 1, do: n * n)
IO.inspect(for x <- [1, 2], y <- ~w(a b), do: {x, y})
IO.inspect(for {:ok, value} <- [{:ok, 1}, :error, {:ok, 3}], do: value)
IO.inspect(for s <- ["ab", "cd"], into: "", do: s)
IO.inspect(for x <- [1, 2, 2, 3], into: MapSet.new(), do: x)

IO.inspect(
  for n <- 1..100, reduce: 0 do
    acc -> if rem(n, 3) == 0, do: acc + n, else: acc
  end
)
```

- Generators work over any Enumerable — lists, ranges, maps (as {key, value} pairs), MapSet, File.stream! output
- Filters are boolean expressions or patterns; a failing pattern quietly skips the element
- Since v1.19 the compiler type-checks that a generator actually implements the Enumerable protocol — wrong types fail at compile time
- for without into: always returns a list; into: picks the collection type

---

## Which construct when?
`exlings exercises: 019–022`

Same input, different shapes of decision — pick by what you are testing: a condition, a value's shape, a chain of matches, or a collection.

The constructs overlap on purpose — Elixir lets you choose the one that states the intent most directly. When in doubt: conditions → if/cond, shapes → case, chains → with, collections → for.

| Construct | Use when | Example hint |
| --- | --- | --- |
| if / else | one condition, two branches | if valid?, do: save(user) |
| unless | nothing new — soft-deprecated since v1.17, write if not | if not valid?, do: retry() |
| cond | several boolean conditions, first truthy wins | score thresholds → letter grade |
| case | match one value against patterns (+ guards) | {:ok, _} vs {:error, _} |
| with | a pipeline of matches on the happy path | parse → validate → save |
| for | transform / filter / accumulate an Enumerable | for n <- 1..10, do: n * 2 |
| try/rescue | handle raised exceptions at boundaries | see Errors & Exceptions |

---

# Enum & Streams

*The workhorse module, the pipe operator that glues it together, and the lazy Stream for when the data gets big — or infinite.*

---

## Enum essentials
`since v1.20 · exlings exercises: 028–035`

Enum runs eagerly over any Enumerable and always returns new data — inputs are never mutated. A dozen functions cover most day-to-day data shaping.

Every Enum function takes the data as the first argument and returns a new value. Start with map/filter/reduce, then let sort_by, group_by and friends replace hand-rolled loops. Since v1.20 Enum.min_max/2 accepts a sorter.

**The core iteration set**

```elixir
orders = [
  %{id: 1, total: 30, user: "ada"},
  %{id: 2, total: 120, user: "grace"},
  %{id: 3, total: 60, user: "linus"}
]

Enum.map(orders, & &1.total)              #=> [30, 120, 60]
Enum.filter(orders, &(&1.total > 50))     #=> orders 2 and 3
Enum.find(orders, &(&1.total > 100))      #=> %{id: 2, ...}
Enum.count(orders, &(&1.total > 50))      #=> 2
Enum.any?(orders, &(&1.total > 100))      #=> true
Enum.all?(orders, &(&1.total > 0))        #=> true
Enum.reduce(orders, 0, &(&1.total + &2))  #=> 210

IO.inspect(Enum.map(orders, & &1.total))
IO.inspect(Enum.count(orders, &(&1.total > 50)))
IO.inspect(Enum.find(orders, &(&1.total > 100)))
IO.inspect(Enum.reduce(orders, 0, &(&1.total + &2)))
```

**Shaping and splitting**

```elixir
orders = [
  %{id: 1, total: 30, user: "ada"},
  %{id: 2, total: 120, user: "grace"},
  %{id: 3, total: 60, user: "linus"}
]

Enum.sort_by(orders, & &1.total, :desc)   #=> biggest first
Enum.group_by(orders, & &1.user)          #=> %{"ada" => [...], ...}
Enum.split_with(orders, &(&1.total > 50)) #=> {big, small}
Enum.chunk_every([1, 2, 3, 4, 5], 2)      #=> [[1, 2], [3, 4], [5]]
Enum.frequencies(~w(a b a c))             #=> %{"a" => 2, "b" => 1, "c" => 1}

# min_by/2 and max_by/2 pick by a key; min_max/1 works on ordered values
Enum.min_by(orders, & &1.total)         #=> the cheapest order
Enum.max_by(orders, & &1.total)         #=> the priciest order
Enum.min_max([10, 20, 30])              #=> {10, 30}
Enum.sum([10, 20, 30])                    #=> 60

IO.inspect(Enum.map(Enum.sort_by(orders, & &1.total, :desc), & &1.id))
IO.inspect(Map.keys(Enum.group_by(orders, & &1.user)))
IO.inspect(Enum.chunk_every([1, 2, 3, 4, 5], 2))
IO.inspect(Enum.frequencies(~w(a b a c)))
IO.inspect(Enum.map([Enum.min_by(orders, & &1.total), Enum.max_by(orders, & &1.total)], & &1.id))
IO.inspect(Enum.min_max([10, 20, 30]))
IO.inspect(Enum.sum([10, 20, 30]))
```

---

## The pipe operator: |>
`exlings exercises: 028–035`

|> inserts the previous result as the FIRST argument of the next call — data transformation reads top to bottom. Since v1.20 dbg shows every intermediate step.

x |> f(a) is exactly f(x, a). Pipelines shine when each step takes the data first — which is why Enum functions are designed that way. Steps that need the value elsewhere take then/2 or a wrapper fn.

**A pipeline reads like the problem**

```elixir
"data.csv"
|> File.read!()
|> String.split("\n")
|> Enum.map(&String.trim/1)
|> Enum.reject(&(&1 == ""))
|> Enum.count()
|> IO.inspect(label: "lines")
```

> Same code without pipes: Enum.count(Enum.reject(Enum.map(String.split(File.read!("data.csv"), "\n"), &String.trim/1), &(&1 == ""))) — nested, inside-out, unreadable.

**Debugging pipes and non-first-arg steps**

```elixir
[3, 1, 2]
|> Enum.sort()
|> dbg()          # since v1.20: prints EVERY step with its result
|> Enum.sum()
#=> 6

port =
  System.get_env("ELIXIR_CHEATS_PORT")   # unset -> nil
  |> then(fn
    nil -> "4000"
    port -> port
  end)
  |> String.to_integer()

IO.inspect(port)
```

> dbg/1 returns its argument, so it can sit anywhere in the pipe — since v1.20 it prints each surrounding step with its intermediate result. then/2 covers branching and value-not-first steps.

---

## Streams — lazy & composable
`exlings exercises: 048–050`

Stream functions build a lazy pipeline: nothing runs until an Enum function forces it. One element flows through at a time — constant memory, even for infinite data.

**Laziness and constant-memory file processing**

```elixir
# lazy: nothing runs until Enum.take pulls values
1..1_000_000_000
|> Stream.map(&(&1 * 3))
|> Stream.filter(&(rem(&1, 2) == 0))
|> Enum.take(3)
#=> [6, 12, 18]

# stream a large file line by line — constant memory
File.stream!("logs/app.log")
|> Stream.map(&String.trim_trailing(&1, "\n"))
|> Stream.filter(&String.contains?(&1, "ERROR"))
|> Enum.take(5)
|> IO.inspect(label: "errors")
```

> [!WARNING]
> Every Stream function returns a lazy %Stream{} — a pipeline ending at Stream.map has computed nothing. Close it with an Enum function (to_list, take, run, each) or a for comprehension to actually execute it.

- Enum: eager — each step builds a full intermediate list; fastest for small, in-memory data
- Stream: lazy — one element flows through the whole pipeline; best for huge, infinite or IO-backed data
- Three composed Enum steps walk and copy the data three times; one Stream walks it once
- Since v1.20, Stream.cycle/1 raises on an empty list instead of misbehaving deep inside a later run

---

## Comprehensions vs Enum pipelines
`exlings exercises: 041–043`

Two styles, one result: for walks the data once and reads like a spec; Enum pipelines compose eager steps. Pick whichever reads better.

for and Enum pipelines overlap heavily. Comprehensions avoid intermediate lists and handle pattern filters naturally; Enum pipelines are easier to extend step by step and to split into named functions.

**for — single pass**

```elixir
squares =
  for n <- 1..10, rem(n, 2) == 0 do
    n * n
  end
#=> [4, 16, 36, 64, 100]
```

**Enum — composable steps**

```elixir
squares =
  1..10
  |> Enum.filter(&(rem(&1, 2) == 0))
  |> Enum.map(&(&1 * &1))
#=> [4, 16, 36, 64, 100]
```

> Same result, two styles. Rule of thumb: reach for for when one pass over patterns reads clearest, for Enum when the steps deserve names or reuse.

---

## Essential Enum functions
`exlings exercises: 028–035`

The daily-driver table: what each function does and what comes back. Everything here works on any Enumerable and returns new data.

Arities shown are the common ones — most functions have a variant with an extra argument (a sorter, a default, a :nofile-style option). Bookmark h Enum in IEx for the full set.

| Function | What it does | Returns |
| --- | --- | --- |
| Enum.map/2 | apply a function to every element | new list |
| Enum.filter/2 | keep elements where the function is truthy | new list |
| Enum.reduce/3 | fold every element into one accumulator | accumulator |
| Enum.each/2 | run a function for side effects | :ok |
| Enum.find/2 | first element where the function is truthy | element or nil |
| Enum.any?/2 / all?/2 | do some / all elements pass? | boolean |
| Enum.sort_by/3 | order by a computed key, asc or desc | new list |
| Enum.group_by/2 | bucket elements by a computed key | map of key → list |
| Enum.split_with/2 | partition into matching and the rest | {matches, rest} |
| Enum.chunk_every/2 | slice into fixed-size groups | list of lists |
| Enum.frequencies/1 | count occurrences of each element | map of element → count |
| Enum.min_max/2 | smallest and largest by a sorter | {min, max} — sorter since v1.20 |

---

# Recursion

*No while, no loop statements — pattern-matched clauses are the loop, accumulators make tail calls constant-stack, and Enum.reduce generalizes it all.*

---

## Body recursion over lists
`exlings exercises: 036–040`

There are no loops in Elixir: a recursion is clauses — a base case for [] and a recursive case for [head | tail].

Data is immutable and there is no loop syntax — instead, define the function in clauses: one for the smallest input (the base case) and one that reduces the problem ([head | tail]) and calls itself. Immutability guarantees the list shrinks every call, so termination comes from the data.

**Sum and map, by hand**

```elixir
defmodule MyList do
  # base case: empty list — stops the recursion
  def sum([]), do: 0

  # recursive case: peel the head, recurse on the tail
  def sum([head | tail]), do: head + sum(tail)

  def square([]), do: []
  def square([head | tail]), do: [head * head | square(tail)]
end

MyList.sum([1, 2, 3, 4])      #=> 10
MyList.square([1, 2, 3, 4])   #=> [1, 4, 9, 16]

IO.inspect(MyList.sum([1, 2, 3, 4]))
IO.inspect(MyList.square([1, 2, 3, 4]))
```

> [!WARNING]
> A recursion with no reachable base case loops forever. On the BEAM there is no StackOverflow to bail you out — the process keeps allocating until memory kills it. Always write and test the smallest input (empty list, 0, nil) first.

---

## Tail calls & accumulators
`exlings exercises: 036–040`

Move the work into an accumulator so the recursive call is the LAST operation — BEAM tail-call optimization then runs it in constant stack space.

In body recursion each stack frame waits for the recursive result. Make the recursive call the last thing the function does and carry partial results in an accumulator — the BEAM then reuses the single frame, giving O(1) stack regardless of input size.

**Body recursion — stack grows**

```elixir
defmodule Body do
  def len([]), do: 0
  def len([_ | tail]), do: 1 + len(tail)
end

# 1 + (1 + (1 + ...)) — every frame waits for
# the next call, so the stack grows with the list
```

**Tail recursion — constant stack**

```elixir
defmodule Tail do
  def len(list), do: go(list, 0)

  defp go([], acc), do: acc
  defp go([_ | tail], acc), do: go(tail, acc + 1)
end

# acc carries the result; the recursive call IS
# the last operation, so BEAM reuses the frame
```

**The Enum.reverse/1 idiom**

```elixir
defmodule Doubler do
  def run(list), do: go(list, [])

  defp go([], acc), do: Enum.reverse(acc)      # restore order
  defp go([head | tail], acc), do: go(tail, [head * 2 | acc])
end

Doubler.run([1, 2, 3])  #=> [2, 4, 6]

IO.inspect(Doubler.run([1, 2, 3]))
```

> Prepending to the accumulator is O(1); appending is O(n) — so accumulate reversed and finish with the classic Enum.reverse/1.

---

## reduce as universal recursion
`exlings exercises: 028–035`

An accumulator threaded through clauses is exactly Enum.reduce — map, filter, sum and friends are all special cases of it.

Before hand-writing recursive clauses over a collection, ask: is this an accumulator being threaded through? Then Enum.reduce/3 states it once — no base case to forget, and the intent is obvious to every Elixirist.

**The same recursions, declaratively**

```elixir
Enum.reduce([1, 2, 3, 4], 0, &+/2)
#=> 10 — the recursive sum, one line

squares =
  [1, 2, 3, 4]
  |> Enum.reduce([], &[&1 * &1 | &2])
  |> Enum.reverse()
#=> [1, 4, 9, 16]

IO.inspect(Enum.reduce([1, 2, 3, 4], 0, &+/2))
IO.inspect([1, 2, 3, 4] |> Enum.reduce([], &[&1 * &1 | &2]) |> Enum.reverse())
```

> Writing custom recursion is still the right tool for multiple shapes, early exits, or traversals reduce cannot express — but it is now a deliberate choice.

---

## Recursion over multiple shapes
`exlings exercises: 036–040`

Clauses can dispatch on several shapes at once — multiple base cases, nested structures, guards — but every path needs a terminating clause.

Base cases are where correctness lives — enumerate them first (0, 1, [], missing keys), then the shapes that recurse. Multiple base cases and nested data fall out of the same clause style.

**Two base cases + nested shapes**

```elixir
defmodule Nested do
  # multiple base cases
  def fib(0), do: 0
  def fib(1), do: 1
  def fib(n) when is_integer(n) and n > 1, do: fib(n - 1) + fib(n - 2)

  # recursion over nested shapes
  def flatten([]), do: []
  def flatten([head | tail]) when is_list(head),
    do: flatten(head) ++ flatten(tail)
  def flatten([head | tail]), do: [head | flatten(tail)]
end

Nested.flatten([1, [2, [3, 4]], 5])  #=> [1, 2, 3, 4, 5]

IO.inspect(Enum.map(0..10, &Nested.fib/1))
IO.inspect(Nested.flatten([1, [2, [3, 4]], 5]))
```

---

# Errors & Exceptions

*Tagged tuples for expected failures, raise for bugs, supervisors for crashes — plus a v1.20 type checker that finds broken error handling at compile time.*

---

## Error values vs bang functions
`exlings exercises: 060–064`

Expected failures come back as {:error, reason}; bang (!) variants raise instead. try/rescue is for boundary code — the anatomy comes in the next entry.

Elixir splits failure into two styles: values for failures the caller should handle, exceptions for bugs it should not. Choose the style by asking: would a reasonable caller want to recover from this? (Error-handling exercises in exlings: 060–064.)

**Two failure styles side by side**

```elixir
user = %{name: "Ada", role: :admin}

Map.fetch(user, :age)      #=> :error — absence is a value
Map.fetch(user, :name)     #=> {:ok, "Ada"}

Map.fetch!(user, :role)    #=> :admin — bang raises when missing
File.read("config.toml")   #=> {:ok, data} | {:error, :enoent}
File.read!("config.toml")  #=> data, or ** (File.Error)

IO.inspect(Map.fetch(user, :age))
IO.inspect(Map.fetch(user, :name))
IO.inspect(Map.fetch!(user, :role))
IO.inspect(File.read("config.toml"))

try do
  File.read!("missing.toml")
rescue
  e -> IO.puts("rescued: " <> Exception.message(e))
end
```

- Expected failure → {:ok, result} / {:error, reason} — callers match on it
- Bug / impossible state → raise — let it crash (see let-it-crash below)
- Bang variants raise on failure: Map.fetch!/2, File.read!/1, JSON.decode!/1
- Non-bang return tagged tuples: Map.fetch/2, File.read/1, JSON.decode/1
- try/rescue is for crossing boundaries — convert to a response or log and re-raise

---

## raise, defexception & try anatomy
`exlings exercises: 060–064`

raise throws an exception struct; defexception defines your own. try has the full anatomy: rescue matches exception types, else handles success, after always cleans up.

An exception is just a struct. defexception defines one with fields and a message formatter; raise builds (or re-raises) it. Rescue clauses pattern-match on the exception type and bind the struct.

**Custom exceptions**

```elixir
defmodule PaymentError do
  defexception [:message, :reason]

  @impl true
  def message(%__MODULE__{message: msg, reason: reason}) do
    "#{msg} (reason: #{inspect(reason)})"
  end
end

# raise "simple message"                # RuntimeError
# raise PaymentError,
#   message: "declined",
#   reason: :insufficient_funds
#=> ** (PaymentError) declined (reason: :insufficient_funds)

try do
  raise "simple message"
rescue
  e -> IO.puts("RuntimeError: " <> Exception.message(e))
end

try do
  raise PaymentError, message: "declined", reason: :insufficient_funds
rescue
  e in PaymentError -> IO.puts("PaymentError: " <> Exception.message(e))
end
```

**try / rescue / else / after — full anatomy**

```elixir
result =
  try do
    {:ok, JSON.decode!(File.read!("settings.json"))}
  rescue
    e in File.Error -> {:error, {:file, e.reason}}
    e in JSON.DecodeError -> {:error, {:json, Exception.message(e)}}
  else
    {:ok, data} -> {:ok, data["theme"]}
  after
    IO.puts("attempt finished")     # always runs — cleanup
  end

IO.inspect(result)
```

> Order is do → rescue → else → after. else pattern-matches the value when nothing raised; after runs on success, raise, throw and exit alike.

---

## throw / catch & exit
`exlings exercises: 060–064`

throw is an escape hatch for libraries that must abort deep inside a computation — Enum uses it to stop enumerating early. exit/1 terminates a process on purpose. App code should use neither.

throw sends any value up to the nearest matching catch; exit/1 ends the current process with a reason. Linked processes can trap exits and receive {:EXIT, pid, reason} messages instead of dying.

**catch matches the thrown value; exit ends a process**

```elixir
# catch pattern-matches the thrown value
try do
  throw(:early)
catch
  :early -> :caught_it
end
#=> :caught_it

# exit/1 ends the current process on purpose
Process.flag(:trap_exit, true)   # parent survives; gets :EXIT messages
pid = spawn_link(fn -> exit(:boom) end)

receive do
  {:EXIT, ^pid, :boom} -> :child_exited
end
#=> :child_exited

IO.inspect(
  try do
    throw(:early)
  catch
    :early -> :caught_it
  end
)

Process.flag(:trap_exit, true)   # parent survives; gets :EXIT messages
pid = spawn_link(fn -> exit(:boom) end)

receive do
  {:EXIT, ^pid, :boom} -> IO.puts("child_exited")
end
```

**OK — return error values**

```elixir
def parse_age(params) do
  case Integer.parse(params["age"]) do
    {age, ""} -> {:ok, age}
    _ -> {:error, :invalid_age}
  end
end
```

**Anti-pattern — throw for control flow**

```elixir
def parse_age(params) do
  case Integer.parse(params["age"]) do
    {age, ""} -> {:ok, age}
    _ -> throw({:error, :invalid_age})  # every caller must catch
  end
end
```

> throw skips normal returns: one forgotten catch wrapper turns a handled error into a nocatch crash. Keep {:error, reason} for expected failures.

> [!WARNING]
> In application code prefer error values for expected failures and raise for bugs. throw is reserved for libraries that must abort deep inside a computation — Enum itself throws to exit enumerations early. An uncaught throw crashes with a nocatch error; exit/1 is a shutdown, not an error report.

---

## The let-it-crash philosophy
`exlings exercises: 060–064`

Expected failures are values, bugs are crashes: let the process die and let a supervisor restart it in a known-good state.

The BEAM draws a hard line that most languages blur: failures the caller can handle are values, and everything else is a bug that should crash — loudly, early, and in a process whose state dies with it. Don't defend against bugs with rescue-all; isolate them and restart clean.

- Expected (user typo, missing file) → return {:error, reason} and let callers decide
- Bug (impossible clause, corrupt state) → raise — supervisors restart, telemetry reports
- One process crashing does not kill the node — links and supervisors scope the blast radius
- Convert at boundaries: web plugs turn crashes into 500s, GenServer.call turns them into exits
- Restart strategies (:one_for_one, :rest_for_one) live in the supervision tree — see Processes & OTP

---

## v1.20 type checker × error handling
`since v1.20 · exlings exercises: 060–064`

The gradual type checker understands the error conventions: it warns when a call can be proven to always raise, flags dead clauses, and proves error paths total — no annotations.

Since v1.20 inference of every construct means the compiler reasons about failure itself: which clauses are reachable, which calls can only raise, whether the error path is handled. Broken error handling becomes a compile-time warning instead of a 3 a.m. pager.

**Provable bugs, found for free**

```elixir
defmodule Users do
  def name!(user) do
    Map.fetch!(user, :name)
  end

  def role(user) do
    case Map.fetch(user, :role) do
      {:ok, role} -> role
      :error -> :guest
      other -> other               # v1.20: dead clause warning
    end
  end
end

# since v1.20 the compiler proves this call can only raise:
#   Users.name!(%{})   # warning: %{} can never have :name
Users.name!(%{name: "Ada"})        #=> "Ada"

IO.inspect(Users.role(%{role: :admin}))
IO.inspect(Users.name!(%{name: "Ada"}))

try do
  Users.name!(%{})
rescue
  e -> IO.puts("rescued: " <> Exception.message(e))
end
```

> Map.fetch! needs a map with the :name key; %{} is disjoint from that type, so the call is flagged before it ever runs.

- Disjoint-type violations — arguments that can never satisfy the function, like Map.fetch!/2 on a proven-missing key
- Dead code — clauses after a catch-all and branches after a raise are flagged as redundant
- Narrowing through error paths — handling the nil case removes nil from later clauses, so the happy path is provably safe
- Zero annotations — warnings appear during a plain mix compile

---

# Processes & OTP

*The BEAM runs your code in thousands of tiny isolated processes that talk by message passing. OTP builds production-grade processes from that primitive: GenServer, Supervisor, Task, Agent, Registry.*

---

## spawn, send & receive

Processes are spawned from functions, exchange immutable messages, and each has a mailbox. State lives in a recursive loop — the argument is the state.

**Spawn a process and talk to it**

```elixir
parent = self()

_pid = spawn(fn ->
  receive do                    # wait for a message in the mailbox
    {:hello, from} -> send(from, :hi)
  after
    5_000 -> :timeout           # give up after 5 seconds
  end
end)

send(_pid, {:hello, parent})    # the child receives it and answers the sender
receive do
  :hi -> :got_a_reply
end
|> IO.inspect()
```

> send/2 never blocks — messages are copied into the mailbox. receive scans the mailbox top-down; unhandled messages stay in the mailbox.

**The state-in-argument loop pattern**

```elixir
defmodule Counter do
  def loop(count) do            # the argument is the process state
    receive do
      {:incr, by} -> loop(count + by)
      :stop -> :ok              # no recursive call = process exits
      {:get, caller} ->
        send(caller, {:count, count})
        loop(count)
    end
  end
end

counter = spawn(Counter, :loop, [0])
send(counter, {:incr, 2})
send(counter, {:get, self()})
receive do {:count, n} -> n end  #=> 2
|> IO.inspect()
```

> Processes are extremely cheap: heaps of hundreds of thousands are normal, each with its own garbage collection. Crashing one never corrupts another.

---

## Links, monitors & exit signals

spawn_link ties two processes together (both die together); Process.monitor/1 gets a :DOWN message instead. trap_exit converts exit signals into messages.

**Link vs monitor**

```elixir
# a link is bidirectional: a crash in the child kills this process too
# spawn_link(fn -> raise "boom" end)

Process.flag(:trap_exit, true)   # convert exit signals to messages
child = spawn_link(fn -> Process.sleep(:infinity) end)
Process.exit(child, :kill)
receive do
  {:EXIT, ^child, reason} -> reason
end
|> IO.inspect(label: "exit")

ref = Process.monitor(child)     # monitor is one-way and stackable
receive do
  {:DOWN, ^ref, :process, _pid, reason} -> reason
end
|> IO.inspect(label: "down")
```

- link — bidirectional; exit signals propagate both ways (default: both die)
- monitor — unidirectional; you receive one {:DOWN, ref, :process, pid, reason}
- trap_exit — Process.flag(:trap_exit, true) turns exit signals into {:EXIT, ...} messages
- OTP behaviours (GenServer, Task, Supervisor) do this wiring for you — reach for raw links mostly in libraries

---

## Task & Agent — lightweight OTP

Task = one-off async computation with error propagation; Agent = a tiny state holder. Both are GenServers under the hood — no hand-written receive loops.

**Task — async/await and concurrent streams**

```elixir
defmodule Work do
  def heavy(n), do: n * n
end

task = Task.async(fn -> Work.heavy(1) end)   # spawn + monitor + link
result = Task.await(task, 5_000)             # raises on crash or timeout
result
|> IO.inspect(label: "await")

# run one function over many items, bounded concurrency:
1..10
|> Task.async_stream(&Work.heavy(&1),
  max_concurrency: System.schedulers_online(),
  timeout: 10_000
)
|> Enum.to_list()                     # [{:ok, _} | {:exit, _}, ...]
|> Enum.map(fn {:ok, value} -> value end)
|> IO.inspect(label: "stream")
```

> Task.async/await links the task to the caller — if it crashes, the caller crashes too. That is the good kind of failure in Elixir.

**Agent — background state without boilerplate**

```elixir
{:ok, agent} = Agent.start_link(fn -> %{} end)

Agent.update(agent, &Map.put(&1, :visits, 1))
Agent.get(agent, &Map.get(&1, :visits))         #=> 1

Agent.get_and_update(agent, fn state ->
  # return {value_to_return, new_state}
  {Map.get(state, :visits, 0),
   Map.update(state, :visits, 1, &(&1 + 1))}
end)
|> IO.inspect(label: "get_and_update")

Agent.get(agent, &Map.get(&1, :visits))
|> IO.inspect(label: "after")
```

> For state that must survive crashes or needs access policies, graduate to a GenServer supervised by your tree.

---

## GenServer — the workhorse

use GenServer + client API + callbacks: init/1, handle_call/3 (sync), handle_cast/2 (async). Register with a name so callers need no PID.

**Minimal complete GenServer**

```elixir
defmodule Stack do
  use GenServer

  ## Client API — runs in the CALLER process
  def start_link(initial),
    do: GenServer.start_link(__MODULE__, initial, name: __MODULE__)

  def push(item), do: GenServer.cast(__MODULE__, {:push, item})
  def pop, do: GenServer.call(__MODULE__, :pop)

  ## Callbacks — run in the SERVER process
  @impl true
  def init(initial), do: {:ok, initial}

  @impl true
  def handle_call(:pop, _from, [head | tail]), do: {:reply, head, tail}
  def handle_call(:pop, _from, []), do: {:reply, nil, []}

  @impl true
  def handle_cast({:push, item}, state), do: {:noreply, [item | state]}
end
```

> name: __MODULE__ registers a local name (one per node); {:global, term} or a {:via, Registry, {...}} tuple for wider scopes.

**Using it**

```elixir
{:ok, _} = Stack.start_link([1, 2])
Stack.push(3)
Stack.pop()   #=> 3
Stack.pop()   #=> 2
```

- call — synchronous, backpressure, timeout (default 5000ms); use for reads and anything the caller must wait on
- cast — fire-and-forget; no confirmation, no backpressure — use sparingly
- {:reply, reply, state} / {:noreply, state} — answer now, or later via GenServer.reply/2
- {:stop, reason, state} — shut down cleanly; your Supervisor will restart it

---

## Supervisors — let it crash

A Supervisor starts children, restarts them when they die, and gives up (taking the whole tree down) when restarts exceed the intensity limit.

**A supervision tree root**

```elixir
defmodule MyApp.Supervisor do
  use Supervisor

  def start_link(arg),
    do: Supervisor.start_link(__MODULE__, arg, name: __MODULE__)

  @impl true
  def init(_arg) do
    children = [
      MyApp.Cache,                        # child_spec/1 from use GenServer
      {MyApp.Worker, name: MyApp.Worker}  # module + start argument
    ]

    Supervisor.init(children,
      strategy: :one_for_one,
      max_restarts: 3,     # default: 3 restarts...
      max_seconds: 5       # ...per 5 seconds, else the supervisor aborts
    )
  end
end
```

| Strategy | On child crash | Use when |
| --- | --- | --- |
| :one_for_one | restarts only the crashed child | children are independent |
| :one_for_all | restarts every child | children depend on each other |
| :rest_for_one | restarts the crashed child and all started after it | ordered start dependencies |
| DynamicSupervisor | runtime start/stop of children | unbounded, dynamically added workers |

> [!WARNING]
> If a child exceeds max_restarts within max_seconds (default 3 in 5s), the supervisor itself terminates — the failure climbs the tree instead of hot-looping. Fixes belong in the failing child, not in bigger limits.

---

## Registry & ETS — shared names and data

Registry gives processes lookupable names and pubsub dispatch; ETS is an in-memory key-value table shared by all processes. Both are read-fast, write-safe primitives.

**Registry — names and dispatch**

```elixir
{:ok, _} = Registry.start_link(keys: :unique, name: MyApp.Registry)

# name a process through the registry with a via tuple:
{:ok, agent} =
  Agent.start_link(fn -> 0 end,
    name: {:via, Registry, {MyApp.Registry, {:counter, 42}}}
  )

Registry.lookup(MyApp.Registry, {:counter, 42})   #=> [{pid, nil}]

Registry.register(MyApp.Registry, :topic, nil)    # unique: one subscriber
Registry.dispatch(MyApp.Registry, :topic, fn entries ->
  for {pid, meta} <- entries, do: send(pid, {:event, meta})
end)
```

> Since v1.20, registries with keys: :duplicate are backed by an :ordered_set table — faster dispatch and lower memory for large fan-out.

**ETS — shared key-value table**

```elixir
table = :ets.new(:cache, [:set, :public, read_concurrency: true])

:ets.insert(table, {:ada, "pioneer"})
:ets.lookup(table, :ada)           #=> [ada: "pioneer"]
:ets.insert(table, {:ada, "v2"})   # insert overwrites — writes are atomic
:ets.delete(table, :ada)

IO.inspect(:ets.lookup(table, :ada))
IO.inspect(:ets.lookup(table, :ada) |> length())
:ets.insert(table, {:ada, "v2"})
IO.inspect(:ets.lookup(table, :ada))
:ets.delete(table, :ada)
IO.inspect(:ets.lookup(table, :ada))
```

> ETS lives outside any process (owned by one, survives its readers). Reads are concurrent; coordinate writes through the owner process or accept last-write-wins.

| Need | Reach for |
| --- | --- |
| name processes, find them, broadcast events | Registry — OTP-managed, no races |
| read-mostly cache or lookup table | :ets with :public + read_concurrency |
| fast counters shared by processes | :ets.update_counter/4 — atomic |
| stateful, consistent business logic | GenServer / Agent — not ETS |

---

# Types & the Type System

*Since v1.20 Elixir is a gradually typed language: every program is inferred and type checked with zero annotations. Typespecs and Dialyzer still exist — the built-in checker now catches verified bugs for free at compile time.*

---

## Gradual typing — zero annotations
`since v1.20`

Since v1.20 the compiler infers types for all constructs and warns about verified bugs and dead code — no annotations, no setup, no new syntax.

**The compiler type checks code you never annotated**

```elixir
defmodule Math do
  def square(n), do: n * n          # inferred: number() -> number()

  def run(word), do: square(String.length(word))

  def broken do
    square(:oops)   # type violation: atom() can never be number()
  end
end
```

**Redundant clauses and dead code are warnings now**

```elixir
def handle(msg) do
  case msg do
    {:ok, value} -> value
    {:error, reason} -> reason
    _other -> :unknown   # warning: this clause can never match
  end
end
```

- Everything is inferred — variables, functions, structs, guards, even anonymous functions and macros results
- Only verified findings are reported — warnings fire when the compiler can prove a type violation, never on maybes
- Dead code detection — unreachable clauses and impossible branches are flagged
- Narrowing benchmark — Elixir v1.20 passes 12 of 13 categories in the If T: narrowing benchmark suite
- Zero-config: it runs on mix compile and in the editor through the language server

---

## The dynamic() type
`since v1.20`

dynamic() is gradual typing done right: unlike static any, a violation is reported whenever the supplied and accepted types are provably disjoint — and usage narrows it.

- Static any — unchecked: passing any where number() is expected never errors
- dynamic() — compatible with everything except when the types are provably disjoint (e.g. an atom() flowing where only binary() can be accepted)
- Narrowing by usage — the compiler refines the type from how the value is used

**Usage narrows the type**

```elixir
def total(data) do
  # data starts as dynamic(); adding fields a and b
  # narrows data to %{..., a: number(), b: number()}
  data.a + data.b
end
```

**Annotating gradual types in specs**

```elixir
@spec size_of(dynamic(integer() or binary())) :: integer()
def size_of(value) when is_integer(value), do: value
def size_of(value) when is_binary(value), do: byte_size(value)

# passing an atom() to size_of/1 is a violation:
# atom() is disjoint from integer() or binary()
```

---

## Guards, clauses & occurrence typing
`since v1.20`

Guards refine types per clause: is_map_key/2 proves a key exists, negations become not_set(), and earlier clauses narrow later ones (occurrence typing).

**Guards feed the type system**

```elixir
def first(list, default)
      when is_list(list) and is_integer(default) do
  # inferred here: list :: list(), default :: integer()
  case list do
    [] -> default
    [head | _] -> head
  end
end
```

**is_map_key proves (and disproves) key presence**

```elixir
def name(user) when is_map_key(user, :name) do
  user.name          # OK: user narrowed to %{..., name: dynamic()}
end

def without_retries(config) when not is_map_key(config, :retries) do
  config.retries     # violation: :retries is not_set() in this clause
end

def small?(tuple) when tuple_size(tuple) < 3 do
  elem(tuple, 3)     # violation: size is 0..2, index 3 is impossible
end
```

**Occurrence typing across case/cond/with clauses**

```elixir
def upcase(value) do
  case value do
    nil -> "nothing"   # nil is removed from value in later clauses
    other -> String.upcase(other)  # value cannot be nil here
  end
end
```

> Union, intersection and negation of types flow through guards and clauses — including case, cond, with and multi-clause functions.

---

## Map domain keys
`since v1.20`

Non-atom map keys get real types: %{123 => "hello"} is %{integer() => binary()} — and Map.put/delete/replace results are tracked key by key.

**Domain keys are typed, and can mix with atom keys**

```elixir
sizes = %{123 => "hello"}
# inferred: %{integer() => binary()}

mixed = %{root: 1, 2 => 2}
# inferred: %{integer() => integer(), root: integer()}
```

**Map.put/delete/change are key-tracked**

```elixir
def enable(map), do: Map.put(map, :debug, true)  # :debug is tracked

def drop(map) do
  map = Map.delete(map, :debug)
  map.debug      # violation: :debug is not_set() after Map.delete/2
end

def swap(map), do: Map.replace(map, :debug, false)
# Map.replace/3 keeps the key only if it was already set — if_set()
```

**Map.fetch!/2 is checked like direct access**

```elixir
def name!(map) do
  Map.fetch!(map, :name)   # type-checked like map.name
end

name!(%{})            # warning: :name can never be present in %{}
name!(%{name: "Ada"}) #=> "Ada"
```

---

## Typespecs & Dialyzer

@spec/@type document contracts for humans and tools; the built-in checker (v1.20) needs none of them — Dialyzer remains the static-analysis veteran.

**Typespecs at a glance**

```elixir
defmodule Lexer do
  @type token :: {:word, String.t()} | {:int, integer()}
  @typep state :: %{tokens: [token()]}     # private to this module
  @opaque ast :: {term(), [token()]}       # internals are hidden

  @callback lex(String.t()) :: {:ok, ast()} | {:error, term()}

  @spec tokenize(String.t()) :: [token()]
  def tokenize(input) do
    input
    |> String.split(" ")
    |> Enum.map(&{:word, &1})
  end
end
```

> mix dialyzer (via the dialyxir package) runs Erlang success typing over your specs and code.

| Aspect | Dialyzer (dialyxir) | Built-in checker (v1.20) |
| --- | --- | --- |
| Setup | add dep, build PLT cache first | none — runs with mix compile |
| Annotations | optional @spec-driven success typing | none needed — infers all code |
| Findings | potential flaws inferred from specs | only verified bugs + dead code |
| Speed | slow on first PLT, cached after | incremental — type-checker caching |
| Runs where | separate mix dialyzer task | compile, mix test, editor inline |

---

## Compiler options for types & speed
`since v1.20`

v1.20 adds module_definition: :interpreted for faster compiles of huge modules, and replaces xref: [exclude:] with no_warn_undefined.

**mix.exs — project compiler options**

```elixir
def project do
  [
    app: :big_app,
    elixirc_options: [
      module_definition: :interpreted,   # faster builds for huge modules
      no_warn_undefined: Legacy.Adapter  # replaces xref: [exclude: ...]
    ]
  ]
end
```

- :module_definition — :compiled (default) vs :interpreted; interpreted mode speeds up compilation of very large projects when the type checker becomes the bottleneck
- no_warn_undefined — silences undefined-function warnings for optional dependencies and dynamic calls
- xref: [exclude: ...] in mix.exs is hard-deprecated since v1.20 — migrate to elixirc_options: [no_warn_undefined: ...]
- Profiling — per-module type-check times via compiler profile: :time (v1.20.2+) to find slow modules

---

# Mix & Testing

*Mix is the build tool and task runner; ExUnit ships with it. Learn the task surface (newer discovery helpers included), the assertion toolkit, and the conventions that make test suites fast and green.*

---

## Mix essentials & task discovery

Project scaffolding, dependency management and app introspection — plus newer discovery helpers like mix help Mod.fun/arity (v1.19) and mix source MODULE (v1.20).

**Everyday Mix**

```sh
mix new my_app --sup             # OTP app + supervision tree
mix new my_lib --umbrella        # umbrella project
mix deps                         # list dependencies and their status
mix deps.get                     # fetch dependencies
mix deps.update decimal          # update one dep (mix deps.update --all: all)
mix app.tree                     # print the runtime application tree
mix app.tree --output tree.txt   # since v1.20: write the tree to a file
MIX_OS_DEPS_COMPILE_PARTITION_COUNT=4 mix deps.compile   # v1.19+: parallel deps
```

**Finding tasks and docs**

```sh
mix help                        # every available task
mix help mix format             # docs for a specific task
mix help String.trim/1          # module or fun/arity docs (since v1.19)
mix help app:package            # tasks listed per application (since v1.19)
mix source Ecto.Query           # print/open a module source (since v1.20)
```

> Since v1.20, mix help output also renders @type and @callback documentation for the module you ask about.

---

## ExUnit basics
`exlings exercises: 075`

use ExUnit.Case, test + assert/refute, describe blocks, tags and setup — with --dry-run (v1.20) and run counts on flaky retries (v1.20).

**A test module**

```elixir
ExUnit.start(seed: 1, max_cases: 1, autorun: false)

defmodule Calc do
  def add(a, b), do: a + b

  defmodule Cache do
    use Agent

    def start_link(_opts), do: Agent.start_link(fn -> %{} end, name: __MODULE__)
  end
end

defmodule CalcTest do
  use ExUnit.Case, async: true

  describe "add/2" do
    @tag :smoke
    test "adds numbers" do
      assert Calc.add(1, 2) == 3
      refute Calc.add(1, 2) == 4
    end

    test "knows its floats", _context do
      assert Calc.add(0.1, 0.2) != 0.3   # classic float surprise
    end
  end

  setup do
    [cache: start_supervised!(Calc.Cache)]  # per-test, auto-stopped
  end
end

ExUnit.run()
```

**Running tests**

```sh
mix test                             # everything
mix test test/calc_test.exs          # one file
mix test test/calc_test.exs:12       # the test that contains line 12
mix test --only smoke                # tagged tests only
mix test --exclude smoke             # everything except a tag
mix test --dry-run                   # since v1.20: list tests, run none
mix test --repeat-until-failure 100  # since v1.20: prints remaining runs while retrying
```

> --repeat-until-failure N reruns the suite until it fails (flaky hunting); v1.20 shows how many runs remain as it goes.

---

## Assertions & doctests
`exlings exercises: 075`

assert match?/2 for patterns, assert_in_delta for floats, assert_raise for errors — plus doctests that turn @doc examples into tests.

**The assertion toolkit**

```elixir
ExUnit.start(seed: 1, max_cases: 1, autorun: false)

defmodule ToolkitTest do
  use ExUnit.Case, async: true

  test "the assertion toolkit" do
    user = %{name: "Ada"}

    assert match?(%{name: "Ada"}, user)      # does a pattern match?
    assert_in_delta 0.3, 1 / 3, 0.001        # float-safe comparison
    assert_raise ArithmeticError, fn -> div(1, 0) end
    assert_raise RuntimeError, ~r/boom/, fn -> raise "boom" end

    refute Enum.empty?([1])                  # assert not
  end
end

ExUnit.run()
```

> Since v1.18 ExUnit diffs are much smarter — structural, colored comparisons for maps, lists and strings instead of inspect dumps.

**Doctests — examples that must stay true**

```elixir
defmodule Greeting do
  @doc """
  Builds a greeting.

      iex> Greeting.build("Ada")
      "Hello, Ada!"
  """
  def build(name), do: "Hello, #{name}!"
end

defmodule GreetingTest do
  use ExUnit.Case, async: true
  doctest Greeting      # each iex> example becomes a test
end

ExUnit.start(seed: 1, max_cases: 1, autorun: false)
ExUnit.run()
```

---

## Properties of good tests

Opinionated defaults: run everything async, supervise every process you start, wire heavy flows with aliases, and test behaviour — not internals.

- async: true everywhere — tests run on all scheduler cores; keep them free of global state and shared files
- start_supervised!/1 — start processes per test with automatic cleanup; never leak start_link results into other tests
- Aliases in mix.exs — compose flows like "test": ["ecto.create --quiet", "ecto.migrate", "test"]
- Fixtures/factories — small builder functions over plain maps/structs; avoid one giant mutable fixture file
- Test behaviour, not implementation — assert on outputs and messages so refactors stay cheap
- Isolate slow things — tag with @tag :slow and split with mix test --exclude slow in the fast loop

---

## Formatting & docs

mix format is non-negotiable community standard; --migrate modernizes code (v1.15), --no-compile skips recompiles (v1.20), and mix help now renders types/callbacks (v1.20).

**Formatting commands**

```sh
mix format                    # format the whole project
mix format lib/my_app.ex      # one file
mix format --check-formatted  # CI gate: exit 1 if anything is unformatted
mix format --migrate          # since v1.15: also modernizes deprecated code
mix format --no-compile       # since v1.20: format without compiling after
```

**.formatter.exs**

```elixir
[
  import_deps: [:ecto, :phoenix],
  line_length: 98,
  inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
```

> import_deps pulls in each dependency's formatter config so macros like Ecto queries indent correctly.

**Documentation conventions**

```elixir
defmodule Math do
  @moduledoc """
  Helpers for integer math.
  """

  @doc """
  Doubles a number.

      iex> Math.twice(21)
      42
  """
  def twice(n), do: n * 2

  @spec even?(integer()) :: boolean()
  def even?(n), do: rem(n, 2) == 0
end

Math.twice(21)
|> IO.inspect()
Math.even?(42)
|> IO.inspect()
```

> @moduledoc and @doc accept heredocs; examples in iex> style double as doctests. Since v1.20, mix help Math prints docs for types and callbacks too.

---

# Protocols & Behaviours

*Protocols give you polymorphism by data type (dispatch on the first argument at runtime); behaviours give you contracts by module (checked at compile time). Elixir extension points, in two flavors.*

---

## defprotocol / defimpl
`exlings exercises: 065–067`

Define a protocol once, implement it per data type. String.Chars (for to_string/1 and interpolation) and Inspect are the ones you will touch most.

**A protocol and two implementations**

```elixir
defprotocol Size do
  @doc "Elements, bytes — depends on the data"
  def size(data)
end

defimpl Size, for: Map do
  def size(map), do: map_size(map)
end

defimpl Size, for: BitString do
  def size(bin), do: byte_size(bin)
end

Size.size(%{a: 1, b: 2})   #=> 2
Size.size("hello")         #=> 5

IO.inspect(Size.size(%{a: 1, b: 2}))
IO.inspect(Size.size("hello"))
```

**Built-ins: derive vs manual implementation**

```elixir
defmodule Account do
  @enforce_keys [:id]
  @derive {Inspect, only: [:id]}   # derive: trim what Inspect shows
  defstruct [:id, plan: :free]
end

# implement manually for interpolation and to_string/1:
defimpl String.Chars, for: Account do
  def to_string(account), do: "Account##{account.id} (#{account.plan})"
end
defmodule Show do
  def run do
    to_string(%Account{id: 7})   #=> Account#7 (free)

    IO.inspect(to_string(%Account{id: 7}))
    IO.inspect(%Account{id: 7, plan: :pro})   # @derive Inspect trimmed the fields
  end
end

Show.run()
```

> Other built-ins: Enumerable (Enum/Stream), Collectable, List.Chars. Structs can also derive Enumerable or Jason.Encoder-style protocols from libraries.

---

## Dispatch type checking
`since v1.19`

Since v1.19 the compiler type checks protocol dispatch: interpolation warns when the type cannot implement String.Chars, for warns for non-Enumerable input.

**Caught before Protocol.UndefinedError**

```elixir
defmodule Demo do
  def interpolate(range) do
    "#{range}"   # warning: range() does not implement String.Chars
  end

  def iterate(number) do
    for x <- number, do: x * 2   # warning: integer() is not Enumerable
  end
end
```

> These warnings come from protocol dispatch sites being type checked — the same machinery that now flags verified bugs since v1.20. Implementing a protocol for: Any (see fallback) silences them.

- Interpolation #{value} requires String.Chars — ranges, tuples and PIDs will warn (v1.19+)
- for x <- collection requires Enumerable — passing a plain integer warns (v1.19+)
- Fix by converting explicitly: Enum.to_list/1, Kernel.to_string/1, or implement the protocol for your type

---

## @fallback_to_any — handle with care
`exlings exercises: 065–067`

A protocol can fall back to an implementation for: Any, but doing so disables dispatch warnings and can hide bugs — use it only for truly universal defaults.

**Fallback declaration**

```elixir
defprotocol Blank do
  @fallback_to_any true
  def blank?(data)
end

defimpl Blank, for: Any do
  def blank?(_data), do: false   # everything else is not blank
end

defimpl Blank, for: List do
  def blank?([]), do: true       # a specific impl wins over Any
  def blank?(_), do: false
end

Blank.blank?([])    #=> true  (List impl)
Blank.blank?(7)     #=> false (Any impl)

IO.inspect([Blank.blank?([]), Blank.blank?(7), Blank.blank?(%{})])
```

> [!WARNING]
> Implementing for: Any makes every type dispatch to one implementation — so the compiler can no longer prove what is missing, and the v1.19+ dispatch warnings plus real Protocol.UndefinedError bugs get swallowed. Prefer explicit defimpl per type. Mix consolidates protocols automatically (dev, test, releases), so post-consolidation dispatch is a fast direct call — the fallback only broadens what that call accepts.

---

## Behaviours — contracts between modules
`exlings exercises: 068–070`

@callback defines a module contract, @behaviour opts in, @impl true documents intent — and the compiler checks them all. use wires behaviours in via __using__.

**Define and implement a behaviour**

```elixir
defmodule MyApp.Pipeline.Stage do
  @type stage :: term()

  @callback init(opts :: term()) :: stage()
  @callback run(event :: term(), stage()) :: {:ok, term()} | {:error, term()}

  @optional_callbacks init: 1   # not implementing init/1 is fine
end

defmodule MyApp.Pipeline.Log do
  @behaviour MyApp.Pipeline.Stage

  @impl true
  def run(event, state) do
    IO.puts("got #{inspect(event)}")
    {:ok, state}
  end
end

MyApp.Pipeline.Log.run(:click, :state)   #=> got :click
```

> Missing callbacks, wrong arities and stale implementations become compile warnings. @impl true also guards against defining a function nobody asked for.

**use + __using__ injects behaviour wiring**

```elixir
defmodule MyApp.Pipeline.Stage do
  defmacro __using__(opts) do
    quote do
      @behaviour MyApp.Pipeline.Stage
      @default_opts unquote(opts)

      @impl true
      def init(opts), do: Keyword.merge(@default_opts, opts)
      defoverridable init: 1
    end
  end
end

defmodule Log do
  use MyApp.Pipeline.Stage, level: :debug   # behaviour + init injected
end

Log.init([other: 1])
|> IO.inspect(label: "init merged defaults")
```

> GenServer, Supervisor and Task are behaviours — use GenServer injects the behaviour declaration, a child_spec/1 and default callbacks you then override.

---

# Metaprogramming

*Elixir code is data: quoted expressions are ASTs you can build, inspect and inject. Macros run at compile time to generate code — powerful for DSLs, overkill for everything else.*

---

## AST, quote & unquote

quote turns code into its AST — literals represent themselves, everything else is a {form, metadata, args} tuple. Macro.to_string turns ASTs back into code.

**Code as data (REPL)**

```elixir
iex> quote do: 1 + 2
{:+, [context: Elixir, imports: [{1, Kernel}, {2, Kernel}]], [1, 2]}

iex> Code.string_to_quoted!("a + b")
{:+, [line: 1], [{:a, [line: 1], nil}, {:b, [line: 1], nil}]}

iex> Macro.to_string(quote do: Enum.map(list, &(&1 * 2)))
"Enum.map(list, &(&1 * 2))"

IO.inspect(quote(do: 1 + 2))
IO.inspect(Code.string_to_quoted!("a + b"))
IO.inspect(Macro.to_string(quote(do: Enum.map(list, &(&1 * 2)))))
```

- Literals self-represent — atoms, integers, floats, strings and lists are their own AST
- Everything else is a 3-tuple — {form, metadata, args}
- Calls look like {fun, meta, [args]}; variables like {:name, meta, nil}
- Blocks are {:__block__, meta, [statements]} — quote wraps multiple expressions in one
- unquote/1 splices an AST into a quoted expression; it is the only way in

---

## defmacro — compile-time code generation

Macros receive ASTs, not values and expand at compile time. That single fact is why a spliced term can run more than once — and why Macro.escape is needed to splice runtime values.

**A timing macro**

```elixir
defmodule Timing do
  defmacro timing(label, do: block) do
    quote do
      {elapsed, result} = :timer.tc(fn -> unquote(block) end)
      _ = elapsed   # measured; printing it would make the output non-deterministic
      IO.puts("#{unquote(label)}: #{inspect(result)}")
      result
    end
  end
end

defmodule Runner do
  import Timing

  def run, do: timing("sum", do: Enum.sum(1..1_000))
end

Runner.run()   #=> sum: 500500
# the body above was inlined at compile time
```

> The macro also measures the block with :timer.tc; the elapsed time is deliberately not printed, so this example's output is the same on every machine. A macro can only be used from a module defined after it: import Timing at the top level of the file that defines it fails with *module Timing is not loaded but was defined*.

> [!WARNING]
> double(2 + 3) does not hand your macro 5 — it hands you the AST of 2 + 3. Elixir's hygiene wraps spliced AST in parentheses, so precedence is safe (double(2 + 3) is 10). What bites you is repetition: splice the same term twice and its side effects run twice. If you need a value evaluated once, write a function.

**Splicing: what a macro really receives**

```elixir
defmodule Oops do
  # the argument arrives as AST, and it is spliced TWICE
  defmacro double(x), do: quote(do: unquote(x) + unquote(x))
end

defmodule Demo do
  require Oops

  def bump do
    IO.puts("evaluated")
    1
  end

  def run, do: Oops.double(bump())
end

Demo.run()   #=> 2, and "evaluated" printed twice

# precedence is safe: hygiene wraps spliced AST in parens
defmodule Safe do
  require Oops
  def run, do: Oops.double(2 + 3)
end

Safe.run()   #=> 10, not 8
```

> Runtime values (maps, lists) must become AST before splicing: Macro.escape(value). Atoms, numbers and binaries embed directly.

---

## use & __using__

use Module calls its __using__/1 macro and injects the quoted code into your module — how use GenServer wires behaviours, defaults and child specs in one line.

**A tiny __using__ of your own**

```elixir
defmodule MiniLogger do
  defmacro __using__(opts) do
    level = Keyword.get(opts, :level, :info)   # runs at compile time

    quote do
      @level unquote(level)

      def log(message), do: IO.puts("[#{@level}] " <> message)
    end
  end
end

defmodule MyApp do
  use MiniLogger, level: :debug
  def run, do: log("started")    #=> [debug] started
end

MyApp.run()
```

- use = module-level: inject attributes, functions, behaviour declarations (GenServer-style)
- import = function-level: bring existing functions into scope — no code generation
- behaviours = contract-level: the module implements callbacks itself, nothing is injected
- Reach for use last: it hides code, slows the compiler, and makes mix format/editors guess more

---

## Hygiene & rules of thumb

Macro bodies are hygienic — they cannot touch caller variables unless you say var!. And the golden rule stands: if a function can do it, write a function.

**A hygienic macro**

```elixir
defmodule Debug do
  defmacro log_result(expr) do
    quote do
      result = unquote(expr)
      IO.puts("got: #{inspect(result)}")   # result is module-unique
      result
    end
  end
end

defmodule Demo do
  import Debug
  def go, do: log_result(2 + 3)   #=> got: 5 (and returns 5)
end

Demo.go()
```

- var!(name) — deliberately read or assign a variable in the caller scope, escaping hygiene
- import inside quote — control exactly what the caller scope gains from your macro
- Macro.escape/1 — turn runtime values into ASTs before unquote
- Macro.underscore/1, Macro.camelize/1 — the handy name utils live in the Macro module

**Macro (unneeded)**

```elixir
defmodule Bad do
  defmacro greet(name) do
    quote do: "Hello, " <> unquote(name)
  end
end

Bad.greet("Ada")     # works...
# &Bad.greet/1       # compile error: cannot capture a macro
```

**Function (just right)**

```elixir
defmodule Good do
  def greet(name), do: "Hello, " <> name
end

"Ada" |> Good.greet()    # pipes
caps = &Good.greet/1     # captures
Good.greet("Ada")        # plain calls
```

> Macros cannot be captured or passed as values, and every use re-expands at compile time. Functions compose with pipes, captures and higher-order calls.

---

# Gotchas & Pitfalls

*The traps every Elixir newcomer hits at least once: float division, charlists, partial map matches, lazy streams — plus hard deprecations new in v1.19 and v1.20.*

---

## / always returns a float

The / operator always returns a float — even for exact division. Use div/2 and rem/2 for integer math.

**Trapped**

```elixir
10 / 4    #=> 2.5
10 / 5    #=> 2.0   exact division is STILL a float
is_float(10 / 5)   #=> true
10 / 0    #=> ArithmeticError (never :infinity)
```

**Integer math**

```elixir
div(10, 4)   #=> 2     truncates toward zero
rem(10, 4)   #=> 2     sign follows the dividend
div(10, 0)   #=> ArithmeticError
```

> Float.floor/1 or trunc/1 can convert a float result, but div/2 is the idiomatic integer division.

---

## is_atom(nil) is true

Booleans and nil are atoms — true, false, nil are just the atoms :true, :false, :nil with special literals.

> [!WARNING]
> A guard like when is_atom(status) also matches nil, true and false. The v1.20 type checker will not save you here — it is correct behavior. Narrow explicitly when the distinction matters.

```elixir
is_atom(nil)     #=> true   nil is the atom :nil
is_atom(true)    #=> true   booleans are atoms
is_boolean(nil)  #=> false  but not a boolean

# only plain atoms like :ok:
# the plain-atom test:
is_atom(:ok) and not is_boolean(:ok) and not is_nil(:ok)

IO.inspect([
  is_atom(nil),
  is_atom(true),
  is_boolean(nil),
  is_atom(:ok) and not is_boolean(:ok) and not is_nil(:ok),
])
```

---

## Charlist vs string — the single-quote trap

Single quotes create charlists (lists of codepoints), not strings — 'hello' == "hello" is false, and lists of integers print as charlists.

**Charlist**

```elixir
'hi' == "hi"        #=> false
is_list('hi')       #=> true
is_binary('hi')     #=> false
IO.inspect('hi')    #=> 'hi'  looks like a string, is not
[104, 105]          #=> 'hi'  lists of integers print as charlists
```

**String (binary)**

```elixir
"hi" == "hi"       #=> true
is_binary("hi")    #=> true  UTF-8 encoded binary
String.to_charlist("hi")   #=> 'hi'
to_string('hi')    #=> "hi"  convert between the two
```

> Erlang APIs (like :crypto or :filename) expect charlists — that is why they exist. In Elixir code, prefer strings.

---

## Map pattern matching is partial

%{name: _} matches any map containing a :name key — extra keys are ignored. Use structs or a map_size/1 guard when you need strictness.

**Surprising**

```elixir
def name(%{name: name}), do: name

name(%{name: "Ada", junk: 1, extra: 2})
#=> "Ada"  — matched anyway, extra keys ignored

name(%{other: 1})
#=> FunctionClauseError — key truly missing
```

**Strict**

```elixir
def name(%User{name: name}), do: name
# structs match exact keys only

def only_name(map) when map_size(map) == 1,
  do: map.name
# guard rejects maps with more keys
```

> Partial matching is a feature for updating and defaulting — but a trap when you intended to assert the exact shape.

---

## with without else swallows mismatches

When no clause matches, a with without else returns the raw non-matching value silently — always provide an else for errors you care about.

**Silent fallthrough**

```elixir
with {:ok, user} <- fetch(id) do
  user.name
end
# fetch/1 returned {:error, :not_found}?
# the with returns {:error, :not_found} —
# no raise, no warning, do block skipped
```

**Explicit else**

```elixir
with {:ok, user} <- fetch(id) do
  user.name
else
  {:error, reason} ->
    Logger.warning("fetch failed: #{inspect(reason)}")
    {:error, reason}
end
```

> The returned non-matching value flows onward through your pipeline as if it were valid data — the bug surfaces far from the cause.

---

## Regex cannot be a struct field default
`since v1.19`

Since v1.19 a Regex struct can no longer be a struct field default — the compiler rejects defstruct pattern: ~r/foo/; build it in new/1.

**Fails to compile (v1.19+)**

```elixir
defmodule Route do
  defstruct pattern: ~r/foo/i
  # error: invalid default for struct field :pattern
  # regexes are not allowed as struct defaults (v1.19+)
end
```

**Build in new/1**

```elixir
defmodule Route do
  defstruct pattern: nil

  def new(pattern \\ ~r/foo/i) do
    %__MODULE__{pattern: pattern}
  end
end
```

> The same applies to any value that cannot live as a plain literal in compiled struct form.

---

## size(n) in patterns needs a pin
`since v1.20`

In bitstring patterns, size(n) and unit(n) now require the variable to be pinned — <<x::size(^n)>> (hard-deprecated without it since v1.20).

**Old form (hard-deprecated in v1.20)**

```elixir
def header(<<len, body::size(len)>>) do
  # v1.20 warning: size must use the pin operator:
  # expected <<len, body::size(^len)>>
end
```

**Pinned (v1.20+)**

```elixir
def header(<<len, body::size(^len)>>) do
  {:ok, body}   # len was bound by the earlier segment
end
```

> The pin makes the hidden dependency (later segment sized by an earlier one) explicit — and lets the compiler check it.

---

## File.stream!/3 argument order swapped
`since v1.20`

Hard-deprecated in v1.20: the old (path, modes, lines_or_bytes) order became (path, lines_or_bytes, modes).

**Old order (v1.19 and earlier)**

```elixir
File.stream!("app.log", [:read, :utf8], 2048)
# path, modes, lines_or_bytes   <- old order

# v1.20 emits a deprecation warning (or errors)
```

**New order (v1.20+)**

```elixir
File.stream!("app.log", 2048, [:read, :utf8])
# path, lines_or_bytes, modes   <- new order

File.stream!("app.log")        # defaults still fine
```

> File.stream!/1 and File.stream!/2 with a single option list keep working — only the 3-argument form changed order.

---

## Atoms are never garbage-collected

Atoms live forever on the VM — String.to_atom/1 on unbounded user input is a memory-exhaustion DoS. Use String.to_existing_atom/1 at trust boundaries.

**Unbounded atoms**

```elixir
def metric(name) do
  # every unique request mints a PERMANENT atom:
  String.to_atom("metric_" <> name)
end
# attacker sends millions of unique names -> OOM
```

**Existing atoms only**

```elixir
def metric(name) do
  String.to_existing_atom("metric_" <> name)
end
# raises ArgumentError for unknown atoms —
# rescue it, or pre-register the valid set
```

> The BEAM caps total atoms (~1M by default) — but the memory is allocated long before the limit hits.

---

## Enum chains blow up memory — Stream does not

Every chained Enum step builds a full intermediate list; Stream composes lazily — but a stream does nothing until an Enum function pulls it.

> [!WARNING]
> Eager Enum chains over huge collections allocate one intermediate list per step. But the inverse trap is real too: compose a Stream and forget to run it, and you ship a %Stream{} around instead of data — Enum.to_list/1 (or Enum.sum/1, Enum.each/2...) is what actually executes the pipeline.

```elixir
# eager: 10M-element list, then another 5M-element list
1..10_000_000
|> Enum.map(&(&1 * 3))
|> Enum.filter(&rem(&1, 2) == 0)
|> Enum.sum()

# lazy: one pass, constant memory
1..10_000_000
|> Stream.map(&(&1 * 3))
|> Stream.filter(&rem(&1, 2) == 0)
|> Enum.sum()      # <- the Enum call runs the stream

1..1_000_000
|> Enum.map(&(&1 * 3))
|> Enum.filter(&rem(&1, 2) == 0)
|> Enum.sum()
|> IO.inspect(label: "eager")

1..1_000_000
|> Stream.map(&(&1 * 3))
|> Stream.filter(&rem(&1, 2) == 0)
|> Enum.sum()
|> IO.inspect(label: "lazy")
```

---

# Reference Tables

*Pin these to the wall: operator precedence, literals, sigils, guards, the standard library go-to functions, and everything new in Elixir v1.20.*

---

## Operators & precedence

Highest precedence first. and/or require a boolean left argument; &&/|| accept any value where only nil and false are falsy.

| Precedence | Operators | Associativity | Notes |
| --- | --- | --- | --- |
| 1 | . | left | call: mod.fun(arg) |
| 2 | @ | — | module attribute: @doc |
| 3 | + - ! ^ not | right | unary, incl. pin ^x |
| 4 | * / | left | / always yields float |
| 5 | + - | left | binary add/subtract |
| 6 | ++ -- <> | right | list diff, concatenation |
| 7 | in not in | left | membership: x in list |
| 8 | |> | left | pipe into first arg |
| 9 | < > <= >= | left | term comparison |
| 10 | == != | left | loose equality: 1 == 1.0 |
| 11 | === !== | left | strict equality: 1 !== 1.0 |
| 12 | && || | right | short-circuit, truthy |
| 13 | and or | right | short-circuit, boolean left |
| 14 | = | right | match operator |
| 15 | & | — | capture: &(&1 + 1) |
| 16 | | | — | type union in specs |

- == vs === — loose treats 1 == 1.0 as true; strict does not
- and/or vs &&/|| — the former demand a boolean left argument, the latter accept anything
- Use parentheses when mixing comparison with boolean operators — readability beats memorized precedence

---

## Literals quick reference

Numbers with bases and underscores, atoms, strings vs charlists, bitstrings, collections — the syntax surface in one table.

| Literal | Syntax | Notes |
| --- | --- | --- |
| Integer | 1_000_000 0b1010 0o777 0xFF | underscores + bases 2/8/16 |
| Float | 3.14 1.0e-3 | 64-bit; / returns one |
| Atom | :ok :+ :weird_name? | never garbage-collected |
| String | ~s(hello) — double-quoted text | UTF-8 binary; #{} interpolation, <> concat |
| Charlist | 'hello' | list of codepoints: [104, 101, ...] |
| Bitstring / binary | <<1, 2, 3>> <<x::size(8)-unit(1)>> | pattern-matchable segments |
| Tuple | {:ok, value} | fixed size, fast elem/2 |
| List | [1, 2, 3] [head | tail] | singly linked, prepends are O(1) |
| Keyword list | [name: :ada, age: 1] | atom keys, ordered, duplicates ok |
| Map | %{a: 1} %{1 => :one} | mixed keys: %{1 => :x, a: :y} |
| Struct | %User{name: :ada} | defstruct, exact keys, update %{u | name: x} |
| Range | 1..10 1..10//2 | step struct, Enumerable |
| Function | fn x -> x end &Mod.fun/1 | anonymous + capture |

---

## Sigils
`exlings exercises: 071–073`

Sigil letter picks the builder, delimiter picks the quoting, lowercase enables interpolation and escapes. Extras like ~w(a b c)a take a trailing modifier.

| Sigil | Meaning | Example |
| --- | --- | --- |
| ~s | string, interpolated | ~s(hello #{name}) |
| ~S | string, raw — no escapes | ~S(C:\path \n literal) |
| ~c | charlist | ~c(hello) |
| ~w | word list, modifiers a c s | ~w(a b c)a → [:a, :b, :c] |
| ~r | regex, modifiers i u x ... | ~r/\d+/i |
| ~D | Date struct | ~D[2026-06-03] |
| ~T | Time struct | ~T[09:00:00.500] |
| ~N | NaiveDateTime — no zone | ~N[2026-06-03 09:00:00] |
| ~U | DateTime in UTC | ~U[2026-06-03 09:00:00Z] |
| Heredoc | multi-line string | three double-quote delimiters on their own lines |

- Delimiters — ~s(...), ~s[...], ~s{...}, ~s<...>, ~s|...|, ~s/.../ all work
- Lowercase = processed (interpolation + escapes); uppercase = raw
- Custom sigils: define a sigil_x/2 macro (lowercase letter) in a module and import it

---

## Common guards

Built-in type checks usable in when clauses — all safe, side-effect-free, and (since v1.20) feeding the type checker.

| Guard | True when |
| --- | --- |
| is_integer/1 | an integer (0, -3, 0xFF) |
| is_float/1 | a float (2.5) |
| is_number/1 | integer or float |
| is_binary/1 | a binary — strings included |
| is_bitstring/1 | a bitstring (binaries are bitstrings) |
| is_atom/1 | an atom — incl. nil, true, false |
| is_boolean/1 | exactly true or false |
| is_nil/1 | exactly nil |
| is_list/1 | a list (charlists too) |
| is_map/1 | a map (structs included) |
| is_tuple/1 | a tuple |
| is_function/1 | an anonymous or named function |
| is_function/2 | a function of that arity: is_function(f, 2) |
| is_struct/1 | a struct (a map with __struct__) |
| is_struct/2 | a struct of that module: is_struct(x, User) |
| is_map_key/2 | the map has that key — narrows types in v1.20 |
| is_exception/1,2 | an exception struct (optionally of a module) |
| in/2 | x in list or x in range |

- Guards can be combined with and, or, not — and custom guards via defguard
- Only a fixed set of functions is allowed in guards — no String.length/1, no user functions

---

## Essential stdlib

The modules you will open in h every day — one go-to line each. JSON is built in since v1.18; Enum + String + Map cover most day-to-day work.

| Module | Go-to functions |
| --- | --- |
| Kernel | + <> |> hd/1 tl/1 dbg/1 all is_* guards |
| Enum | map/2 filter/2 reduce/3 find/2 sort/2 group_by/2 sum/1 |
| String | split/2 join/2 upcase/1 trim/1 contains?/2 replace/3 |
| Map | get/3 put/3 fetch!/2 merge/2 new/2 keys/1 |
| List | first/1 last/1 flatten/1 wrap/1 duplicate/2 zip/2 |
| Tuple | elem/2 put_elem/3 tuple_size/1 |
| Integer | parse/1 to_string/1 digits/2 ceil_div/2 popcount/1 |
| Float | parse/1 to_string/1 round/2 |
| IO | puts/2 inspect/2 gets/2 iodata_empty?/1 |
| File | read/1 write/3 read!/1 stream!/3 mkdir_p/1 |
| Path | join/2 expand/1 relative/2 extname/1 |
| Process | self/0 alive?/1 monitor/1 flag/2 get_label/1 |
| Task | async/1 await/2 async_stream/3 |
| GenServer | start_link/3 call/3 cast/2 reply/2 |
| Registry | start_link/1 register/3 lookup/2 dispatch/4 |
| JSON | encode!/1 encode/1 decode/2 decode!/2 — built in since v1.18 |

---

## New in v1.20 — quick table
`since v1.20`

The v1.20 additions worth reaching for: sharp List functions, a faster Registry backend, mix source, mix test --dry-run and the gradual type system itself.

| Addition | Where | Since |
| --- | --- | --- |
| Integer.ceil_div/2 | Integer | v1.20 |
| Integer.popcount/1 | Integer | v1.20 |
| List.first!/1 and List.last!/1 | List | v1.20 |
| IO.iodata_empty?/1 | IO | v1.20 |
| Process.get_label/1 | Process | v1.20 |
| Regex.import/1 | Regex | v1.20 |
| Enum.min_max/2 with sorter | Enum | v1.20 |
| File.read/2 accepting [:raw] | File | v1.20 |
| mix source MODULE — print/open source | Mix | v1.20 |
| mix test --dry-run | Mix | v1.20 |
| mix help renders type/callback docs | Mix | v1.20 |
| elixirc_options: [module_definition: ...] | compiler | v1.20 |
| gradual type checking of all code | compiler | v1.20 |
| Registry duplicate keys on :ordered_set | Registry | v1.20 |

- Also breaking in v1.20 — require no longer expands the required module at compile time; raw CR characters in strings or comments are rejected
- Hard-deprecated in v1.20 — old File.stream!/3 order, unpinned size(var) in bit patterns, Logger backends, xref: [exclude:]

---

*From [elixir-cheatsheet.lambdastratum.com](https://elixir-cheatsheet.lambdastratum.com/) — the Elixir v1.20.4 cheatsheet, published by [LambdaStratum](https://lambdastratum.com/). Generated 2026-10-02. Elixir and the Elixir logo are registered trademarks of The Elixir Team; this sheet is not affiliated with them.*
