{
  "meta": {
    "langVersion": "1.20.4",
    "langReleased": "2026-08-28",
    "exercisesVersion": "75 exercises",
    "inspiredBy": "https://cheats.rs/",
    "exlingsUrl": "https://github.com/zoedsoupe/exlings",
    "otpRequirement": "OTP 27+ · 29 compatible",
    "siteUrl": "https://elixir-cheatsheet.lambdastratum.com/",
    "siteHost": "elixir-cheatsheet.lambdastratum.com",
    "publisher": "LambdaStratum",
    "publisherUrl": "https://lambdastratum.com/",
    "publisherLogo": "/lambdastratum.svg"
  },
  "runs": {
    "basics/basics-immutability/0": {
      "output": "map1 (untouched): %{name: \"Ada\", role: :admin}\nmap2 (new): %{name: \"Ada\", role: :user}\nrebound: %{active: true, name: \"Ada\"}",
      "cmd": "elixir main.exs"
    },
    "basics/basics-modules/0": {
      "output": "greet/1: \"Hello, Ada!\"\ngreet/2: \"Ola, Ada!\"",
      "cmd": "elixir main.exs"
    },
    "basics/basics-operators/0": {
      "output": "7\n2.5\n2\n[1, 2, 3, 4]\n[1, 3]\nfoobar\ntrue\ntrue",
      "cmd": "elixir main.exs"
    },
    "basics/basics-strings/0": {
      "output": "interpolated: \"Hello, Ada!\"\nupcase: \"ELIXIR\"\ngraphemes: 5\nbytes: 6",
      "cmd": "elixir main.exs"
    },
    "collections/collections-keyword-lists/0": {
      "output": "true\n[:default, {:ok, :admin}, [:name, :role]]",
      "cmd": "elixir main.exs"
    },
    "collections/collections-keyword-lists/1": {
      "output": "{\"db.local\", 4000}\n{\"db.local\", 5433}",
      "cmd": "elixir main.exs"
    },
    "collections/collections-lists/0": {
      "output": "{3, [1, 2]}\n[3, 3, 2]\n[0, 3, 1, 2]\n[3, 1, 2, 4]\n[1, 3]",
      "cmd": "elixir main.exs"
    },
    "collections/collections-maps/0": {
      "output": "[\"Ada\", :admin, \"Ada\"]\n[\"fallback\", :error]",
      "cmd": "elixir main.exs"
    },
    "collections/collections-maps/1": {
      "output": "true\nMapSet.new([1, 2, 3, 4])\nMapSet.new([2, 3])",
      "cmd": "elixir main.exs"
    },
    "collections/collections-ranges/0": {
      "output": "[true, [10, 20, 30], 2500, true, false]\n[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]",
      "cmd": "elixir main.exs"
    },
    "collections/collections-structs/0": {
      "output": ":member\n%User{name: \"Ada\", role: :admin, tags: []}",
      "cmd": "elixir main.exs"
    },
    "collections/collections-tuples/0": {
      "output": "{:ok, 42}\n{:ok, 43}\n2\n:enoent",
      "cmd": "elixir main.exs"
    },
    "control-flow/control-flow-case/1": {
      "output": "{:ok, :created}",
      "cmd": "elixir main.exs"
    },
    "control-flow/control-flow-case/2": {
      "output": "4000",
      "cmd": "elixir main.exs"
    },
    "control-flow/control-flow-for/0": {
      "output": "[1, 9, 25]\n[{1, \"a\"}, {1, \"b\"}, {2, \"a\"}, {2, \"b\"}]\n[1, 3]\n\"abcd\"\nMapSet.new([1, 2, 3])\n1683",
      "cmd": "elixir main.exs"
    },
    "control-flow/control-flow-if-cond/1": {
      "output": "[\"odd\", :f]",
      "cmd": "elixir main.exs"
    },
    "control-flow/control-flow-with/1": {
      "output": "{:ok, \"Tokyo\"}\n{:error, :missing_key}",
      "cmd": "elixir main.exs"
    },
    "enum-streams/enum-streams-essentials/1": {
      "output": "[30, 120, 60]\n2\n%{id: 2, total: 120, user: \"grace\"}\n210",
      "cmd": "elixir main.exs"
    },
    "enum-streams/enum-streams-essentials/2": {
      "output": "[2, 3, 1]\n[\"ada\", \"grace\", \"linus\"]\n[[1, 2], [3, 4], [5]]\n%{\"a\" => 2, \"b\" => 1, \"c\" => 1}\n[1, 2]\n{10, 30}\n60",
      "cmd": "elixir main.exs"
    },
    "enum-streams/enum-streams-pipe/1": {
      "output": "lines: 4",
      "cmd": "elixir main.exs"
    },
    "enum-streams/enum-streams-pipe/2": {
      "output": "\u001b[36m\u001b[3m[main.exs:3: (file)]\u001b[0m\n\u001b[0m[3, 1, 2]\u001b[2m #=>\u001b[0m [3, 1, 2]\n\u001b[0m\u001b[2m|> \u001b[0mEnum.sort()\u001b[2m #=>\u001b[0m [1, 2, 3]\n\n\u001b[0m4000",
      "cmd": "elixir main.exs"
    },
    "enum-streams/enum-streams-streams/0": {
      "output": "errors: [\"ERROR timeout after 5s\", \"ERROR connection refused\"]",
      "cmd": "elixir main.exs"
    },
    "errors/errors-ok-error/1": {
      "output": ":error\n{:ok, \"Ada\"}\n:admin\n{:ok, \"theme = \\\"dark\\\"\\n\"}\nrescued: could not read file \"missing.toml\": no such file or directory",
      "cmd": "elixir main.exs"
    },
    "errors/errors-raise-rescue/1": {
      "output": "RuntimeError: simple message\nPaymentError: declined (reason: :insufficient_funds)",
      "cmd": "elixir main.exs"
    },
    "errors/errors-raise-rescue/2": {
      "output": "attempt finished\n{:ok, \"dark\"}",
      "cmd": "elixir main.exs"
    },
    "errors/errors-throw-catch-exit/1": {
      "output": ":caught_it\nchild_exited",
      "cmd": "elixir main.exs"
    },
    "errors/errors-type-checker/1": {
      "output": ":admin\n\"Ada\"\nrescued: key :name not found in:\n\n    %{}",
      "cmd": "elixir main.exs"
    },
    "functions/functions-anonymous/1": {
      "output": "[8, :negative, 15]",
      "cmd": "elixir main.exs"
    },
    "functions/functions-capture/1": {
      "output": "[[10, 20, 30], 7, 6, [\"a\", \"b\", \"2\"], \"ELIXIR\"]",
      "cmd": "elixir main.exs"
    },
    "functions/functions-default-args/1": {
      "output": "join/1: \"a, b, c\"\njoin/2: \"a + b + c\"",
      "cmd": "elixir main.exs"
    },
    "functions/functions-guards/1": {
      "output": "[:allowed, :minor, :unknown]",
      "cmd": "elixir main.exs"
    },
    "functions/functions-higher-order/1": {
      "output": "4000\n\"[info] started\"",
      "cmd": "elixir main.exs"
    },
    "functions/functions-named/1": {
      "output": "\"elixir rocks\"",
      "cmd": "elixir main.exs"
    },
    "getting-started/getting-started-script/0": {
      "output": "Hello, World!\nHello, Elixir!\nperson: %{name: \"Ada\", lang: :elixir}",
      "cmd": "elixir main.exs"
    },
    "gotchas/gotchas-enum-vs-stream/1": {
      "output": "eager: 750001500000\nlazy: 750001500000",
      "cmd": "elixir main.exs"
    },
    "gotchas/gotchas-is-atom-nil/1": {
      "output": "[true, true, false, true]",
      "cmd": "elixir main.exs"
    },
    "metaprogramming/metaprogramming-ast-quote/0": {
      "output": "{:+, [context: Elixir, imports: [{1, Kernel}, {2, Kernel}]], [1, 2]}\n{:+, [line: 1], [{:a, [line: 1], nil}, {:b, [line: 1], nil}]}\n\"Enum.map(list, &(&1 * 2))\"",
      "cmd": "elixir main.exs"
    },
    "metaprogramming/metaprogramming-defmacro/0": {
      "output": "sum: 500500",
      "cmd": "elixir main.exs"
    },
    "metaprogramming/metaprogramming-defmacro/2": {
      "output": "evaluated\nevaluated",
      "cmd": "elixir main.exs"
    },
    "metaprogramming/metaprogramming-hygiene/0": {
      "output": "got: 5",
      "cmd": "elixir main.exs"
    },
    "metaprogramming/metaprogramming-use-using/0": {
      "output": "[debug] started",
      "cmd": "elixir main.exs"
    },
    "mix-testing/mix-testing-formatting-docs/2": {
      "output": "42\ntrue",
      "cmd": "elixir main.exs"
    },
    "pattern-matching/pattern-matching-binaries/0": {
      "output": "{1, 5}\n{3, \"abc\"}\n233\n\"abc123\"\n\"el\"",
      "cmd": "elixir main.exs"
    },
    "pattern-matching/pattern-matching-binaries/1": {
      "output": "{:ok, 2048, 1104}\nno match: {:error, :not_a_png}",
      "cmd": "elixir main.exs"
    },
    "pattern-matching/pattern-matching-destructuring/0": {
      "output": "{1, [2, 3]}\n\"Grace\"\n1",
      "cmd": "elixir main.exs"
    },
    "pattern-matching/pattern-matching-function-clauses/0": {
      "output": "[:positive, :zero, :negative, :positive]\ncase: :missing",
      "cmd": "elixir main.exs"
    },
    "pattern-matching/pattern-matching-match-operator/0": {
      "output": "3\n6\n30\nrescued: no match of right hand side value:\n\n    {1, 2}",
      "cmd": "elixir main.exs"
    },
    "pattern-matching/pattern-matching-pin/0": {
      "output": "{2, 9}\nrescued: no match of right hand side value:\n\n    {3, 4}",
      "cmd": "elixir main.exs"
    },
    "primitive-types/primitive-types-atoms/0": {
      "output": "[false, true, \"hello\"]",
      "cmd": "elixir main.exs"
    },
    "primitive-types/primitive-types-atoms/1": {
      "output": "rescued: errors were found at the given arguments:\n\n  * 1st argument: not an already existing atom",
      "cmd": "elixir main.exs"
    },
    "primitive-types/primitive-types-booleans/0": {
      "output": "[true, 0, nil, true, true]\nand demands booleans: expected a boolean on left-side of \"and\", got:\n\n    nil",
      "cmd": "elixir main.exs"
    },
    "primitive-types/primitive-types-dates/0": {
      "output": "[~D[2026-07-03], :lt, 300000]",
      "cmd": "elixir main.exs"
    },
    "primitive-types/primitive-types-numbers/0": {
      "output": "[1000000, 10, 511, 255, 97]\n[2.5, 2, -1]",
      "cmd": "elixir main.exs"
    },
    "primitive-types/primitive-types-numbers/1": {
      "output": "[4, 8, 3.14]\n[{42, \" steps\"}, 42, 2.5]",
      "cmd": "elixir main.exs"
    },
    "primitive-types/primitive-types-sigils/0": {
      "output": "[\"a 2 b\", \"a \\#{1 + 1} b\", [:see, :you, :soon], ~c\"a charlist\"]",
      "cmd": "elixir main.exs"
    },
    "processes-otp/processes-otp-links-monitors/0": {
      "output": "exit: :killed\ndown: :noproc",
      "cmd": "elixir main.exs"
    },
    "processes-otp/processes-otp-registry-ets/1": {
      "output": "[]\n0\n[ada: \"v2\"]\n[]",
      "cmd": "elixir main.exs"
    },
    "processes-otp/processes-otp-spawn-messages/0": {
      "output": ":got_a_reply",
      "cmd": "elixir main.exs"
    },
    "processes-otp/processes-otp-spawn-messages/1": {
      "output": "2",
      "cmd": "elixir main.exs"
    },
    "processes-otp/processes-otp-task-agent/0": {
      "output": "await: 1\nstream: [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]",
      "cmd": "elixir main.exs"
    },
    "processes-otp/processes-otp-task-agent/1": {
      "output": "get_and_update: 1\nafter: 2",
      "cmd": "elixir main.exs"
    },
    "protocols/protocols-behaviours/0": {
      "output": "got :click",
      "cmd": "elixir main.exs"
    },
    "protocols/protocols-behaviours/1": {
      "output": "init merged defaults: [level: :debug, other: 1]",
      "cmd": "elixir main.exs"
    },
    "protocols/protocols-defprotocol/0": {
      "output": "2\n5",
      "cmd": "elixir main.exs"
    },
    "protocols/protocols-defprotocol/1": {
      "output": "\"Account#7 (free)\"\n#Account<id: 7, ...>",
      "cmd": "elixir main.exs"
    },
    "protocols/protocols-fallback-any/0": {
      "output": "[true, false, false]",
      "cmd": "elixir main.exs"
    },
    "recursion/recursion-body/1": {
      "output": "10\n[1, 4, 9, 16]",
      "cmd": "elixir main.exs"
    },
    "recursion/recursion-reduce/1": {
      "output": "10\n[1, 4, 9, 16]",
      "cmd": "elixir main.exs"
    },
    "recursion/recursion-shapes/1": {
      "output": "[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55]\n[1, 2, 3, 4, 5]",
      "cmd": "elixir main.exs"
    },
    "recursion/recursion-tail-calls/2": {
      "output": "[2, 4, 6]",
      "cmd": "elixir main.exs"
    }
  },
  "elixirVersion": "Elixir 1.20.2 (compiled with Erlang/OTP 29) · Erlang/OTP 29",
  "sections": [
    {
      "id": "getting-started",
      "title": "Getting Started",
      "icon": "rocket",
      "blurb": "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.",
      "entries": [
        {
          "id": "getting-started-install",
          "title": "Install Elixir (latest stable)",
          "summary": "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.",
          "keywords": [
            "install",
            "setup",
            "asdf",
            "mise",
            "homebrew",
            "otp",
            "version"
          ],
          "since": null,
          "exercises": "001–003",
          "blocks": [
            {
              "kind": "code",
              "title": "Install via asdf (recommended)",
              "lang": "sh",
              "code": "asdf plugin add erlang https://github.com/asdf-vm/asdf-erlang.git\nasdf plugin add elixir https://github.com/asdf-vm/asdf-elixir.git\nasdf install erlang latest\nasdf install elixir latest\nasdf global elixir latest\n\nelixir --version\n# Erlang/OTP 28 [erts-...] ...\n# Elixir 1.20.4 (compiled with Erlang/OTP 28)"
            },
            {
              "kind": "list",
              "items": [
                "**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"
              ]
            },
            {
              "kind": "warn",
              "title": "OTP is the runtime",
              "content": "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."
            }
          ]
        },
        {
          "id": "getting-started-iex",
          "title": "IEx — the interactive shell",
          "summary": "REPL with autocomplete, history, breakpoints and helpers. The fastest way to poke at the standard library.",
          "keywords": [
            "iex",
            "repl",
            "shell",
            "break",
            "recompile",
            "h",
            "source"
          ],
          "since": "v1.20",
          "exercises": "001–003",
          "blocks": [
            {
              "kind": "code",
              "title": "Everyday IEx",
              "lang": "sh",
              "code": "iex\niex> 1 + 1\n2\niex> h String.split          # documentation for a function\niex> String.split(\"a,b\", \",\")\n[\"a\", \"b\"]\niex> source(String.split)    # print/open source location (since v1.20)\niex> i 1_000_000             # introspect any value's type\niex> v 2                     # recall result of line 2\niex> h()                     # IEx help — lists all helpers"
            },
            {
              "kind": "code",
              "title": "Run a project inside IEx",
              "lang": "sh",
              "code": "iex -S mix          # start IEx with your Mix project compiled + loaded\n# edit code, then:\niex> recompile()    # reload changed modules without leaving the shell",
              "note": "`iex --dbg prye` lets `dbg/1` calls drop you into an interactive pry shell."
            }
          ]
        },
        {
          "id": "getting-started-script",
          "title": "Hello, World — scripts vs projects",
          "summary": "`.exs` scripts run top-to-bottom with `elixir`; real projects use `mix new` and compiled `.ex` sources.",
          "keywords": [
            "hello world",
            "exs",
            "script",
            "mix new",
            "supervised",
            "project"
          ],
          "since": null,
          "exercises": "001–003",
          "blocks": [
            {
              "kind": "code",
              "title": "hello.exs — a script",
              "code": "# Comments start with #\nIO.puts(\"Hello, World!\")\nname = \"Elixir\"\nIO.puts(\"Hello, #{name}!\")   # interpolation with #{…}\n\nperson = %{name: \"Ada\", lang: :elixir}\nIO.inspect(person, label: \"person\")",
              "note": "Run with `elixir hello.exs`. `.exs` = scripted/evaluated each run; `.ex` = compiled to .beam."
            },
            {
              "kind": "code",
              "title": "A real Mix project",
              "lang": "sh",
              "code": "mix new hello --sup          # --sup generates a supervision tree\ncd hello\nmix test                     # the scaffold ships with a passing test\nmix run -e 'IO.puts(\"hi\")'   # run one expression\nMIX_OS_DEPS_COMPILE_PARTITION_COUNT=4 mix deps.compile  # parallel deps (1.19+)",
              "note": "`mix format` formats the whole project; run it before every commit — it is the community standard."
            },
            {
              "kind": "code",
              "title": "What a generated project looks like",
              "code": "defmodule Hello.MixProject do\n  use Mix.Project\n\n  def project do\n    [\n      app: :hello,\n      version: \"0.1.0\",\n      elixir: \"~> 1.20\",\n      start_permanent: Mix.env() == :prod,\n      deps: deps()\n    ]\n  end\n\n  def application do\n    [extra_applications: [:logger], mod: {Hello.Application, []}]\n  end\n\n  defp deps, do: []\nend"
            }
          ]
        },
        {
          "id": "getting-started-map",
          "title": "The whole language in one view",
          "summary": "How the pieces fit: **Kernel** is auto-imported, everything else is a module; data is immutable; functions live in modules; processes do the concurrency.",
          "keywords": [
            "overview",
            "map",
            "kernel",
            "otp",
            "mental model",
            "immutability"
          ],
          "since": null,
          "exercises": "001–050",
          "blocks": [
            {
              "kind": "table",
              "headers": [
                "Layer",
                "What it is",
                "Examples"
              ],
              "rows": [
                [
                  "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"
                ]
              ]
            },
            {
              "kind": "list",
              "items": [
                "**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."
              ]
            }
          ]
        }
      ]
    },
    {
      "id": "basics",
      "title": "Basics",
      "icon": "type",
      "blurb": "Modules, immutability, operators and strings — the everyday grammar of Elixir. Immutability comes early because it changes how you write every function.",
      "entries": [
        {
          "id": "basics-modules",
          "title": "Modules & functions",
          "summary": "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**.",
          "keywords": [
            "defmodule",
            "def",
            "defp",
            "module attribute",
            "alias",
            "require",
            "import",
            "__MODULE__"
          ],
          "since": null,
          "exercises": "009–013",
          "blocks": [
            {
              "kind": "code",
              "title": "defmodule, def, defp, module attributes",
              "code": "defmodule Greet do\n  @moduledoc \"Docs for the whole module.\"\n  @punct \"!\"                # module attribute = compile-time constant\n\n  @doc \"Greets a person by name.\"\n  def greet(name, lang \\\\ :en) do\n    salutation(lang) <> String.capitalize(name) <> @punct\n  end\n\n  defp salutation(:en), do: \"Hello, \"\n  defp salutation(:pt), do: \"Ola, \"\nend\n\nGreet.greet(\"ada\")          #=> \"Hello, Ada!\"\nGreet.greet(\"ada\", :pt)     #=> \"Ola, Ada!\"\n\nIO.inspect(Greet.greet(\"ada\"), label: \"greet/1\")\nIO.inspect(Greet.greet(\"ada\", :pt), label: \"greet/2\")",
              "note": "There is no `return` — the last expression of a clause is its result, and functions are always called as `Module.function(args)`."
            },
            {
              "kind": "list",
              "items": [
                "**`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"
              ]
            }
          ]
        },
        {
          "id": "basics-immutability",
          "title": "Immutability & rebinding",
          "summary": "Data never mutates: functions return **new** values, and `=` only rebinds a name to a different value — every existing value stays intact.",
          "keywords": [
            "immutable",
            "rebinding",
            "map.put",
            "new map",
            "underscore",
            "capture result"
          ],
          "since": null,
          "exercises": "001–003",
          "blocks": [
            {
              "kind": "code",
              "title": "New values, not mutations",
              "code": "map1 = %{name: \"Ada\", role: :admin}\n\nmap2 = Map.put(map1, :role, :user)   # returns a NEW map\n\nmap1\n#=> %{name: \"Ada\", role: :admin}     # map1 is untouched\nmap2\n#=> %{name: \"Ada\", role: :user}\n\n# Idiomatic style: rebind the same name once the old\n# value has no further use\nuser = %{name: \"Ada\"}\nuser = Map.put(user, :active, true)\n\nIO.inspect(map1, label: \"map1 (untouched)\")\nIO.inspect(map2, label: \"map2 (new)\")\nIO.inspect(user, label: \"rebound\")"
            },
            {
              "kind": "list",
              "items": [
                "**`{: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"
              ]
            },
            {
              "kind": "warn",
              "title": "No assignment, no side effects",
              "content": "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."
            }
          ]
        },
        {
          "id": "basics-operators",
          "title": "Operators",
          "summary": "Arithmetic, list and string concatenation, membership — and the difference between value equality `==` and strict equality `===`.",
          "keywords": [
            "arithmetic",
            "div",
            "rem",
            "concatenation",
            "plusplus",
            "in operator",
            "strict equality",
            "comparison"
          ],
          "since": null,
          "exercises": "004–008",
          "blocks": [
            {
              "kind": "code",
              "title": "Everyday operators",
              "code": "iex> 1 + 2 * 3\n7\niex> 10 / 4              # / always returns a float\n2.5\niex> div(10, 4)          # truncated integer division (rem/2 = remainder)\n2\niex> [1, 2] ++ [3, 4]    # list concatenation...\n[1, 2, 3, 4]\niex> [1, 2, 3, 4] -- [2, 4]   # ...and difference\n[1, 3]\niex> \"foo\" <> \"bar\"      # binary (string) concatenation\n\"foobar\"\niex> :elixir in [:erlang, :elixir]   # membership\ntrue\niex> 5 in 1..10          # works on ranges too\ntrue\n\nIO.puts(1 + 2 * 3)\nIO.puts(10 / 4)\nIO.puts(div(10, 4))\nIO.inspect([1, 2] ++ [3, 4])\nIO.inspect([1, 2, 3, 4] -- [2, 4])\nIO.puts(\"foo\" <> \"bar\")\nIO.inspect(:elixir in [:erlang, :elixir])\nIO.inspect(5 in 1..10)"
            },
            {
              "kind": "compare",
              "left": {
                "title": "== — value equality",
                "code": "iex> 1 == 1.0\ntrue\niex> \"a\" == \"a\"\ntrue\niex> :ok == :ok\ntrue"
              },
              "right": {
                "title": "=== — value AND type",
                "code": "iex> 1 === 1.0\nfalse\niex> \"a\" === \"a\"\ntrue\niex> :ok === :ok\ntrue"
              },
              "note": "`==` compares values across the int/float boundary; `===` also requires the same type. Prefer `===` where the distinction matters (list indexes, map keys)."
            },
            {
              "kind": "text",
              "content": "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."
            }
          ]
        },
        {
          "id": "basics-logical-operators",
          "title": "and/or vs &&/||",
          "summary": "`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.",
          "keywords": [
            "and",
            "or",
            "not",
            "boolean",
            "truthy",
            "guards",
            "short-circuit"
          ],
          "since": null,
          "exercises": null,
          "blocks": [
            {
              "kind": "compare",
              "left": {
                "title": "Strict — and / or / not",
                "code": "iex> true and :ok\n:ok\niex> false or :ok\n:ok\niex> not false\ntrue\niex> nil and :ok\n** (BadBooleanError) expected a boolean on left-side of \"and\", got: nil"
              },
              "right": {
                "title": "Truthy — && / || / !",
                "code": "iex> 1 && \"then\"         # any non-false/nil is truthy\n\"then\"\niex> nil || \"else\"\n\"else\"\niex> !nil\ntrue\niex> [1] || :never\n[1]"
              },
              "note": "Only `and`/`or`/`not` compile inside `when` guards — `&&`/`||`/`!` are rejected there. Both flavours short-circuit."
            },
            {
              "kind": "list",
              "items": [
                "`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"
              ]
            }
          ]
        },
        {
          "id": "basics-strings",
          "title": "Strings quick start",
          "summary": "Double-quoted UTF-8 binaries: interpolate with `#{}`, concatenate with `<>`, go multi-line with triple-quote heredocs.",
          "keywords": [
            "interpolation",
            "heredoc",
            "concatenation",
            "graphemes",
            "byte size",
            "string.chars"
          ],
          "since": null,
          "exercises": "044–047",
          "blocks": [
            {
              "kind": "code",
              "title": "Interpolation, concatenation, heredocs",
              "code": "name = \"Ada\"\n\n\"Hello, #{name}!\"        # interpolation — runs String.Chars.to_string/1\n\"Hello, \" <> name        # concatenation (binaries only)\n\nsql = \"\"\"\n  SELECT *\n  FROM users\n  \"\"\"                    # heredoc: multi-line, indentation trimmed\n\nString.upcase(\"elixir\")  #=> \"ELIXIR\"\nString.length(\"héllo\")   #=> 5   (graphemes)\nbyte_size(\"héllo\")       #=> 6   (bytes: é is 2 in UTF-8)\n\nIO.inspect(\"Hello, #{name}!\", label: \"interpolated\")\nIO.inspect(String.upcase(\"elixir\"), label: \"upcase\")\nIO.inspect(String.length(\"héllo\"), label: \"graphemes\")\nIO.inspect(byte_size(\"héllo\"), label: \"bytes\")"
            },
            {
              "kind": "warn",
              "title": "Not everything interpolates",
              "content": "`#{}` 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)."
            }
          ]
        }
      ]
    },
    {
      "id": "primitive-types",
      "title": "Primitive Types",
      "icon": "hash",
      "blurb": "Numbers, atoms, booleans, binaries vs charlists, sigils and dates — the primitive data types and the gotchas each one hides.",
      "entries": [
        {
          "id": "primitive-types-numbers",
          "title": "Numbers",
          "summary": "Integers and floats with underscores, base prefixes and codepoint syntax; `/` always floats — use `div/2` and `rem/2` for integer math.",
          "keywords": [
            "integer",
            "float",
            "hexadecimal",
            "binary literal",
            "octal",
            "codepoint",
            "division",
            "rounding"
          ],
          "since": null,
          "exercises": "004–008",
          "blocks": [
            {
              "kind": "code",
              "title": "Literals",
              "code": "iex> 1_000_000             # underscores for readability\n1000000\niex> 0b1010                # base 2\n10\niex> 0o777                 # base 8\n511\niex> 0xFF                  # base 16\n255\niex> ?a                    # codepoint of a character\n97\niex> 10 / 4                # / is float division\n2.5\niex> div(10, 4)            # truncated integer division\n2\niex> rem(-10, 3)           # remainder keeps the dividend's sign\n-1\n\nIO.inspect([1_000_000, 0b1010, 0o777, 0xFF, ?a])\nIO.inspect([10 / 4, div(10, 4), rem(-10, 3)])"
            },
            {
              "kind": "code",
              "title": "Parsing, rounding, bit tricks",
              "code": "iex> Integer.ceil_div(7, 2)     # division rounded up (since v1.20)\n4\niex> Integer.popcount(255)      # count of set bits (since v1.20)\n8\niex> Float.round(3.14159, 2)\n3.14\niex> Integer.parse(\"42 steps\")  # leading integer + leftover\n{42, \" steps\"}\niex> String.to_integer(\"42\")\n42\niex> String.to_float(\"2.5\")     # requires a dot — \"2\" raises\n2.5\n\nIO.inspect([Integer.ceil_div(7, 2), Integer.popcount(255), Float.round(3.14159, 2)])\nIO.inspect([Integer.parse(\"42 steps\"), String.to_integer(\"42\"), String.to_float(\"2.5\")])",
              "note": "`div/2` and `rem/2` are auto-imported from Kernel; `Integer.ceil_div/2` and `Integer.popcount/1` are new in v1.20."
            }
          ]
        },
        {
          "id": "primitive-types-atoms",
          "title": "Atoms",
          "summary": "Constants whose name is their value — `:ok`, `:error`, and every module name. Great as tags, dangerous when created from user input.",
          "keywords": [
            "atom",
            "alias",
            "to_atom",
            "to_existing_atom",
            "garbage collection",
            "constant"
          ],
          "since": null,
          "exercises": null,
          "blocks": [
            {
              "kind": "code",
              "title": "Atoms are name-is-the-value constants",
              "code": "iex> :ok                      # an atom's name is its value\n:ok\niex> :ok == :error\nfalse\niex> String == :\"Elixir.String\"   # module aliases are atoms too\ntrue\niex> to_string(:hello)            # atoms convert to strings\n\"hello\"\niex> :\"with spaces\"               # quoted atoms\n:\"with spaces\"\n\nIO.inspect([:ok == :error, String == :\"Elixir.String\", to_string(:hello)])"
            },
            {
              "kind": "code",
              "title": "Creating atoms from strings",
              "code": "iex> String.to_atom(\"user_\" <> \"1\")       # creates a NEW atom\n:\"user_1\"\niex> String.to_existing_atom(\"user_1\")    # only if it already exists\n:\"user_1\"\n\n# The one call that cannot fail politely has to be rescued:\ntry do\n  String.to_existing_atom(\"never_created_123\")\nrescue\n  e in ArgumentError -> IO.puts(\"rescued: #{Exception.message(e)}\")\nend"
            },
            {
              "kind": "warn",
              "title": "Atoms are never garbage-collected",
              "content": "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."
            }
          ]
        },
        {
          "id": "primitive-types-booleans",
          "title": "Booleans & nil",
          "summary": "`true`, `false` and `nil` are atoms with special status; only `nil` and `false` are falsy — everything else is truthy.",
          "keywords": [
            "boolean",
            "nil",
            "truthiness",
            "falsy",
            "is_nil",
            "default value"
          ],
          "since": null,
          "exercises": null,
          "blocks": [
            {
              "kind": "code",
              "title": "Truthiness in practice",
              "code": "iex> true and is_atom(:ok)          # and/or demand booleans\ntrue\niex> nil || false || 0 || \"default\" # only nil and false are falsy\n0\niex> nil && :never_evaluated        # &&/|| accept any value\nnil\niex> is_nil(nil)\ntrue\niex> not is_nil(%{})                # there is no nil?/1 in Elixir 1.20\nfalse\n\nIO.inspect([\n  true and is_atom(:ok),\n  nil || false || 0 || \"default\",\n  nil && :never_evaluated,\n  is_nil(nil),\n  not is_nil(%{}),\n])\n\ntry do\n  nil and :never_evaluated\nrescue\n  e -> IO.puts(\"and demands booleans: \" <> Exception.message(e))\nend",
              "note": "`true`, `false` and `nil` are atoms under the hood (`true == :true`), but specs treat them as the distinct types `boolean()` and `nil`."
            },
            {
              "kind": "list",
              "items": [
                "`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"
              ]
            }
          ]
        },
        {
          "id": "primitive-types-strings-charlists",
          "title": "Strings vs charlists vs binaries",
          "summary": "`\"hello\"` is a UTF-8 binary, `'hello'` is a charlist (list of codepoints) — and `<<...>>` binaries are the primitive underneath it all.",
          "keywords": [
            "binary",
            "charlist",
            "utf-8",
            "codepoint",
            "erlang interop",
            "sigil"
          ],
          "since": null,
          "exercises": null,
          "blocks": [
            {
              "kind": "compare",
              "left": {
                "title": "String — UTF-8 binary",
                "code": "iex> s = \"hello\"\n\"hello\"\niex> is_binary(s)\ntrue\niex> s <> \"!\"               # binaries concatenate with <>\n\"hello!\"\niex> ?h                     # codepoint syntax\n104\niex> ?a == 97\ntrue"
              },
              "right": {
                "title": "Charlist — list of codepoints",
                "code": "iex> c = ~c\"hello\"          # identical to 'hello'\n'hello'\niex> is_list(c)\ntrue\niex> c ++ [33]              # lists concatenate with ++\n'hello!'\niex> hd(c)                  # each element is a codepoint\n104"
              },
              "note": "`'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."
            },
            {
              "kind": "warn",
              "title": "Why does my list print like a string?",
              "content": "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."
            }
          ]
        },
        {
          "id": "primitive-types-sigils",
          "title": "Sigils overview",
          "summary": "Sigils are literal-syntax shorthands: `~s` strings, `~c` charlists, `~w` word lists, `~r` regexes, `~D ~T ~N ~U` dates — uppercase means fully literal.",
          "keywords": [
            "sigil",
            "word list",
            "regex",
            "delimiter",
            "interpolation",
            "verbatim"
          ],
          "since": null,
          "exercises": "071–073",
          "blocks": [
            {
              "kind": "code",
              "title": "The common sigils",
              "code": "iex> ~s(a #{1 + 1} b)          # ~s: string, interpolates\n\"a 2 b\"\niex> ~S(a #{1 + 1} b)          # uppercase: fully literal\n\"a \\#{1 + 1} b\"\niex> ~w(see you soon)a         # words; modifier a/s/c = atoms/strings/charlists\n[:see, :you, :soon]\niex> ~r/ab+c/                  # regex — see the Regex module\n~r/ab+c/\niex> ~c(a charlist)\n'charlist'\n\nIO.inspect([~s(a #{1 + 1} b), ~S(a #{1 + 1} b), ~w(see you soon)a, ~c(a charlist)])"
            },
            {
              "kind": "list",
              "items": [
                "**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"
              ]
            },
            {
              "kind": "warn",
              "title": "Lowercase interpolates, uppercase does not",
              "content": "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."
            }
          ]
        },
        {
          "id": "primitive-types-dates",
          "title": "Dates & time",
          "summary": "Four sigils build the date/time structs; `Date.shift/3` moves whole calendar units, and `to_timeout/1` turns duration lists into milliseconds.",
          "keywords": [
            "date",
            "time",
            "datetime",
            "naive",
            "utc",
            "shift",
            "timeout"
          ],
          "since": null,
          "exercises": null,
          "blocks": [
            {
              "kind": "code",
              "title": "Date, Time, NaiveDateTime, DateTime",
              "code": "iex> date = ~D[2026-06-03]            # Date struct (no time, no zone)\niex> time = ~T[08:30:00.500]          # Time struct\niex> naive = ~N[2026-06-03T08:30:00]  # NaiveDateTime (no zone)\niex> dt = ~U[2026-06-03T08:30:00Z]    # DateTime, always UTC\niex> Date.shift(date, month: 1)       # calendar-safe shift (since v1.17)\n~D[2026-07-03]\niex> DateTime.compare(dt, ~U[2026-12-01T00:00:00Z])\n:lt\niex> to_timeout([minute: 5])          # duration -> milliseconds\n300000\n\nIO.inspect([\n  Date.shift(~D[2026-06-03], month: 1),\n  DateTime.compare(~U[2026-06-03T08:30:00Z], ~U[2026-12-01T00:00:00Z]),\n  to_timeout([minute: 5]),\n])",
              "note": "`DateTime.compare/2` returns `:lt | :eq | :gt`; `to_timeout/1` feeds `Process.sleep/1` and timeouts."
            },
            {
              "kind": "list",
              "items": [
                "**`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"
              ]
            }
          ]
        }
      ]
    },
    {
      "id": "collections",
      "title": "Collections",
      "icon": "layers",
      "blurb": "Lists, tuples, keyword lists, maps, structs and ranges — what each one is good at, and a table for choosing between them.",
      "entries": [
        {
          "id": "collections-lists",
          "title": "Lists",
          "summary": "Singly-linked lists: prepending is O(1), everything else walks the list — so build by prepending and access through `Enum`.",
          "keywords": [
            "list",
            "head",
            "tail",
            "cons",
            "prepend",
            "append",
            "linked list"
          ],
          "since": null,
          "exercises": "023–027",
          "blocks": [
            {
              "kind": "code",
              "title": "Head, tail, concat, access",
              "code": "iex> list = [3, 1, 2]\niex> [head | tail] = list      # head = first element, tail = rest\niex> {head, tail}\n{3, [1, 2]}\niex> hd(list)                  # first element (tl/1 = rest)\n3\niex> [0 | list]                # prepend: O(1)\n[0, 3, 1, 2]\niex> list ++ [4]               # append: O(n) — walks every node\n[3, 1, 2, 4]\niex> [1, 2, 3, 4] -- [2, 4]    # difference\n[1, 3]\niex> length(list)              # O(n) — counts node by node\n3\niex> Enum.at(list, 2)          # O(n) — random access walks the list\n2\n\nIO.inspect({head, tail})\nIO.inspect([hd(list), length(list), Enum.at(list, 2)])\nIO.inspect([0 | list])\nIO.inspect(list ++ [4])\nIO.inspect([1, 2, 3, 4] -- [2, 4])"
            },
            {
              "kind": "compare",
              "left": {
                "title": "Prepend — O(1)",
                "code": "def record(event, log) do\n  # new node points at the existing list\n  [event | log]\nend"
              },
              "right": {
                "title": "Append — O(n)",
                "code": "def record(event, log) do\n  # must copy the whole list to reach the end\n  log ++ [event]\nend"
              },
              "note": "To keep chronological order, prepend while building and call `Enum.reverse/1` once at the end — far cheaper than repeated appends."
            },
            {
              "kind": "text",
              "content": "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."
            }
          ]
        },
        {
          "id": "collections-tuples",
          "title": "Tuples",
          "summary": "Fixed-size, O(1)-access containers — the home of tagged return values like `{:ok, value}` and `{:error, reason}`.",
          "keywords": [
            "tuple",
            "elem",
            "put_elem",
            "tuple_size",
            "tagged tuple",
            "ok error"
          ],
          "since": null,
          "exercises": "023–027",
          "blocks": [
            {
              "kind": "code",
              "title": "elem, put_elem, tuple_size",
              "code": "iex> tuple = {:ok, 42}\niex> elem(tuple, 0)\n:ok\niex> elem(tuple, 1)\n42\niex> put_elem(tuple, 1, 43)   # returns a NEW tuple\n{:ok, 43}\niex> tuple_size(tuple)        # O(1) — size is stored, unlike length/1\n2\niex> {:error, reason} = {:error, :enoent}\niex> reason\n:enoent\n\nIO.inspect({elem(tuple, 0), elem(tuple, 1)})\nIO.inspect(put_elem(tuple, 1, 43))\nIO.inspect(tuple_size(tuple))\nIO.inspect(reason)"
            },
            {
              "kind": "list",
              "items": [
                "**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"
              ]
            }
          ]
        },
        {
          "id": "collections-keyword-lists",
          "title": "Keyword lists",
          "summary": "Ordered `[key: value]` lists of two-tuples: duplicate keys allowed, order preserved — the default for function options and DSLs.",
          "keywords": [
            "keyword list",
            "options",
            "ordered",
            "duplicates",
            "keyword module",
            "last argument"
          ],
          "since": null,
          "exercises": "023–027",
          "blocks": [
            {
              "kind": "code",
              "title": "Sugar for tuples of atoms",
              "code": "iex> kw = [name: \"Ada\", role: :admin]\niex> kw == [{:name, \"Ada\"}, {:role, :admin}]   # sugar for tuples\ntrue\niex> [name: \"Ada\", name: \"Grace\"]              # ordered, duplicates allowed\n[name: \"Ada\", name: \"Grace\"]\niex> Keyword.get(kw, :missing, :default)\n:default\niex> Keyword.fetch(kw, :role)\n{:ok, :admin}\niex> Keyword.keys(kw)\n[:name, :role]\n\nIO.inspect(kw == [{:name, \"Ada\"}, {:role, :admin}])\nIO.inspect([\n  Keyword.get(kw, :missing, :default),\n  Keyword.fetch(kw, :role),\n  Keyword.keys(kw),\n])"
            },
            {
              "kind": "code",
              "title": "The last-argument sugar",
              "code": "defmodule Client do\n  def connect(host, opts \\\\ []) do\n    port = Keyword.get(opts, :port, 4000)\n    {host, port}\n  end\nend\n\nIO.inspect(Client.connect(\"db.local\"))                    # opts = []\nIO.inspect(Client.connect(\"db.local\", port: 5433, timeout: 5_000))\n# a trailing bracket-less keyword list becomes the LAST argument"
            },
            {
              "kind": "text",
              "content": "**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."
            }
          ]
        },
        {
          "id": "collections-maps",
          "title": "Maps & MapSet",
          "summary": "The general key-value store: `%{}` with atom-key sugar or `=>` for any keys — plus `MapSet` for uniqueness and set algebra.",
          "keywords": [
            "map",
            "mapset",
            "atom key",
            "string key",
            "fetch",
            "access",
            "key domain"
          ],
          "since": null,
          "exercises": "023–027",
          "blocks": [
            {
              "kind": "code",
              "title": "Access patterns",
              "code": "iex> m = %{name: \"Ada\", role: :admin}    # atom keys: shorthand\niex> m.name                              # dot access: only for known atom keys\n\"Ada\"\niex> m[:role]                            # bracket syntax: any key type\n:admin\niex> mixed = %{\"a\" => 1, :b => 2, 3 => \"c\"}   # => syntax: any key type\niex> Map.fetch!(m, :name)                # raises KeyError when missing\n\"Ada\"\niex> Map.get(m, :missing, \"fallback\")\n\"fallback\"\niex> Map.fetch(m, :missing)              # :error instead of raising\n:error\n\nIO.inspect([m.name, m[:role], Map.fetch!(m, :name)])\nIO.inspect([Map.get(m, :missing, \"fallback\"), Map.fetch(m, :missing)])",
              "note": "`Map.fetch/2` distinguishes missing (`:error`) from stored-nil; `Map.get/3` and bracket access collapse both into `nil`/default."
            },
            {
              "kind": "code",
              "title": "MapSet — unique members",
              "code": "iex> set = MapSet.new([1, 2, 2, 3])   # duplicates collapse\niex> MapSet.member?(set, 2)\ntrue\niex> MapSet.union(set, MapSet.new([3, 4]))\n#MapSet<[1, 2, 3, 4]>\niex> MapSet.intersection(set, MapSet.new([2, 3, 9]))\n#MapSet<[2, 3]>\n\nIO.inspect(MapSet.member?(set, 2))\nIO.inspect(MapSet.union(set, MapSet.new([3, 4])))\nIO.inspect(MapSet.intersection(set, MapSet.new([2, 3, 9])))"
            },
            {
              "kind": "warn",
              "title": "Dot access is for known atom keys only",
              "content": "`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."
            }
          ]
        },
        {
          "id": "collections-structs",
          "title": "Structs",
          "summary": "Maps with a module attached: `defstruct` fields with defaults, optional compile-time enforcement, and checked update syntax.",
          "keywords": [
            "struct",
            "defstruct",
            "enforce_keys",
            "derive",
            "update syntax",
            "default"
          ],
          "since": null,
          "exercises": "023–027",
          "blocks": [
            {
              "kind": "code",
              "title": "defstruct, enforce_keys, updates",
              "code": "defmodule User do\n  @enforce_keys [:name]\n  defstruct name: nil, role: :member, tags: []\nend\n\ndefmodule Demo do\n  def run do\n    user = %User{name: \"Ada\"}          # missing :name = compile error\n    admin = %User{user | role: :admin} # update syntax: existing fields only\n    IO.inspect(user.role)\n    IO.inspect(admin)\n  end\nend\n\nDemo.run()\n# %User{user | nope: 1}              # unknown key fails to compile (v1.18+)",
              "note": "`%User{user | ...}` is the **update** syntax — it requires an existing struct and only accepts declared fields, verified by the compiler since v1.18."
            },
            {
              "kind": "compare",
              "left": {
                "title": "Banned since v1.19",
                "code": "defmodule Filter do\n  # compile error — regexps are not allowed\n  # as struct defaults\n  defstruct pattern: ~r/foo/i\nend"
              },
              "right": {
                "title": "Compile in a constructor",
                "code": "defmodule Filter do\n  defstruct pattern: nil\n\n  def new(source) do\n    %Filter{pattern: Regex.compile!(source)}\n  end\nend"
              },
              "note": "A struct default must be a literal value; regexps hold runtime resources, so compile them inside a constructor function like `new/1`."
            },
            {
              "kind": "list",
              "items": [
                "**`@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"
              ]
            }
          ]
        },
        {
          "id": "collections-ranges",
          "title": "Ranges — and which collection when?",
          "summary": "`first..last//step` sequences are enumerables — and the fastest way to answer the eternal question: which collection do I reach for?",
          "keywords": [
            "range",
            "step",
            "enumerable",
            "disjoint",
            "descending"
          ],
          "since": null,
          "exercises": null,
          "blocks": [
            {
              "kind": "code",
              "title": "Ranges",
              "code": "iex> range = 1..10              # inclusive, ascending\niex> 5 in range\ntrue\niex> Enum.map(1..3, &(&1 * 10))\n[10, 20, 30]\niex> Enum.sum(1..100//2)        # step: 1, 3, 5, ..., 99\n2500\niex> Enum.to_list(10..1//-1)    # descending: explicit negative step\n[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]\niex> Range.disjoint?(1..5, 6..10)\ntrue\niex> Range.disjoint?(1..5, 5..10)\nfalse\n\nIO.inspect([\n  5 in range,\n  Enum.map(1..3, &(&1 * 10)),\n  Enum.sum(1..100//2),\n  Range.disjoint?(1..5, 6..10),\n  Range.disjoint?(1..5, 5..10),\n])\nIO.inspect(Enum.to_list(10..1//-1))"
            },
            {
              "kind": "table",
              "headers": [
                "Collection",
                "Ordered",
                "Duplicates",
                "Access",
                "Typical use"
              ],
              "rows": [
                [
                  "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"
                ]
              ]
            }
          ]
        }
      ]
    },
    {
      "id": "pattern-matching",
      "title": "Pattern Matching",
      "icon": "shrink",
      "blurb": "The match operator, destructuring, the pin operator, function clauses and binary patterns — the single most-used feature of the language.",
      "entries": [
        {
          "id": "pattern-matching-match-operator",
          "title": "The match operator",
          "summary": "`=` is not assignment: the left side is a **pattern** matched against the right — it binds variables or raises MatchError.",
          "keywords": [
            "match operator",
            "binding",
            "matcherror",
            "repeated variables",
            "equals"
          ],
          "since": null,
          "exercises": "014–018",
          "blocks": [
            {
              "kind": "code",
              "title": "Patterns bind — or fail loudly",
              "code": "iex> {a, b} = {1, 2}          # tuples destructure element-wise\niex> a + b\n3\niex> [x, y, z] = [1, 2, 3]    # lists destructure positionally\niex> x + y + z\n6\niex> {x, x} = {1, 1}          # repeated vars must be equal\niex> %{age: age} = %{age: 30}   # partial match: take the keys you need\n\nIO.inspect(a + b)\nIO.inspect(x + y + z)\nIO.inspect(age)\n\n# A repeated variable is a match condition, not a rebind:\ntry do\n  {x, x} = {1, 2}\nrescue\n  e -> IO.puts(\"rescued: \" <> Exception.message(e))\nend"
            },
            {
              "kind": "text",
              "content": "`=` 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`."
            },
            {
              "kind": "warn",
              "title": "Duplicate keys in a map pattern do not compile",
              "content": "`%{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."
            }
          ]
        },
        {
          "id": "pattern-matching-destructuring",
          "title": "Destructuring",
          "summary": "Pull values out of lists, maps, tuples and any nesting — but remember map patterns are **partial**: extra keys are ignored.",
          "keywords": [
            "destructuring",
            "head tail",
            "nested",
            "partial match",
            "map pattern"
          ],
          "since": null,
          "exercises": "014–018",
          "blocks": [
            {
              "kind": "code",
              "title": "Lists, maps, nesting",
              "code": "iex> [h | t] = [1, 2, 3]\niex> {h, t}\n{1, [2, 3]}\niex> %{name: name} = %{name: \"Ada\", age: 36}   # partial match!\niex> name\n\"Ada\"\niex> %{user: %{name: name}} = %{user: %{name: \"Grace\", admin: true}}\niex> name\n\"Grace\"\niex> {:ok, [first | _rest]} = {:ok, [1, 2, 3]}\niex> first\n1\n\nIO.inspect({h, t})\nIO.inspect(name)   # rebound by the nested match above\nIO.inspect(first)"
            },
            {
              "kind": "list",
              "items": [
                "**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)"
              ]
            },
            {
              "kind": "warn",
              "title": "Map patterns are partial",
              "content": "`%{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."
            }
          ]
        },
        {
          "id": "pattern-matching-pin",
          "title": "The pin operator",
          "summary": "`^var` matches against an **existing** binding instead of creating a new one; `_` never binds, `_var` binds quietly.",
          "keywords": [
            "pin operator",
            "caret",
            "underscore",
            "ignored variable",
            "rebind"
          ],
          "since": null,
          "exercises": "014–018",
          "blocks": [
            {
              "kind": "code",
              "title": "Pinning in matches",
              "code": "iex> expected = 2\niex> {^expected, actual} = {2, 9}     # ^ reads the existing binding\niex> {expected, actual}\n{2, 9}\n\nIO.inspect({expected, actual})\n\n# A pin that does not match is a MatchError, not a rebind:\ntry do\n  {^expected, _} = {3, 4}\nrescue\n  e -> IO.puts(\"rescued: \" <> Exception.message(e))\nend"
            },
            {
              "kind": "compare",
              "left": {
                "title": "Wrong — silently rebinds",
                "code": "status = :pending\n\n# no pin: this creates a NEW binding\n{status, other} = {:done, 1}\n\nstatus\n#=> :done  — :pending was lost"
              },
              "right": {
                "title": "Right — pin with ^",
                "code": "status = :pending\n\n# ^ reads the EXISTING binding\n{^status, other} = {:done, 1}\n#=> ** (MatchError) — :pending does not match :done"
              },
              "note": "`_` 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."
            }
          ]
        },
        {
          "id": "pattern-matching-function-clauses",
          "title": "Function clauses & case",
          "summary": "Multiple clauses try top to bottom and the first match wins — so order specific patterns before general ones, and use `when` guards to refine.",
          "keywords": [
            "function clauses",
            "case",
            "guards",
            "when",
            "first match",
            "functionclauseerror",
            "caseclauseerror"
          ],
          "since": null,
          "exercises": "014–018",
          "blocks": [
            {
              "kind": "code",
              "title": "Multi-clause functions and case",
              "code": "defmodule Math do\n  def sign(n) when n > 0, do: :positive    # guard refines the clause\n  def sign(0), do: :zero                   # literal pattern\n  def sign(n) when n < 0, do: :negative\n  def sign(_), do: {:error, :not_a_number} # catch-all last\nend\n\ndefmodule Demo do\n  def run do\n    IO.inspect([Math.sign(3), Math.sign(0), Math.sign(-2), Math.sign(\"x\")])\n\n    case File.read(\"mix.exs\") do\n      {:ok, content} -> {:read, byte_size(content)}\n      {:error, :enoent} -> :missing\n      {:error, reason} -> {:failed, reason}    # cover every shape\n    end\n    |> IO.inspect(label: \"case\")\n  end\nend\n\nDemo.run()"
            },
            {
              "kind": "compare",
              "left": {
                "title": "Wrong — general clause first",
                "code": "def handle({:error, reason}), do: {:log, reason}\ndef handle({:error, :enoent}), do: :missing\n\n# the second clause can NEVER match —\n# the compiler even warns about it"
              },
              "right": {
                "title": "Right — specific first",
                "code": "def handle({:error, :enoent}), do: :missing\ndef handle({:error, reason}), do: {:log, reason}\ndef handle(:ok), do: :ok\n\n# no matching clause at all ->\n# FunctionClauseError at runtime"
              },
              "note": "Clauses try **top to bottom** — put specific patterns before general ones. A `case` that runs out of clauses raises `CaseClauseError`."
            },
            {
              "kind": "text",
              "content": "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."
            }
          ]
        },
        {
          "id": "pattern-matching-binaries",
          "title": "Binary patterns",
          "summary": "Match binaries byte-for-byte with `<<...>>`: fixed widths, `::binary` tails, `::utf8` codepoints and string prefix patterns.",
          "keywords": [
            "binary pattern",
            "bitstring",
            "utf8",
            "binary-size",
            "endian",
            "header parsing"
          ],
          "since": null,
          "exercises": "014–018",
          "blocks": [
            {
              "kind": "code",
              "title": "Sizes, utf8, prefixes",
              "code": "iex> <<a, b>> = <<1, 2>>        # each segment defaults to 8 bits\niex> {a, b}\n{1, 2}\niex> <<len, rest::binary>> = <<3, 97, 98, 99>>\niex> {len, rest}\n{3, \"abc\"}\niex> <<a::8, b::16, c::8>> = <<1, 0, 5, 9>>   # bit-sized segments\niex> {a, b, c}\n{1, 5, 9}\niex> <<ch::utf8>> = \"é\"         # one UTF-8 codepoint (2 bytes here)\niex> ch\n233\niex> \"Bearer \" <> token = \"Bearer abc123\"     # prefix match on strings\niex> token\n\"abc123\"\niex> <<w1::binary-size(2), _::binary>> = \"elixir\"\niex> w1\n\"el\"\n\nIO.inspect({a, b})\nIO.inspect({len, rest})\nIO.inspect(ch)\nIO.inspect(token)\nIO.inspect(w1)"
            },
            {
              "kind": "code",
              "title": "Parsing a binary file header",
              "code": "defmodule PNG do\n  @magic <<137, 80, 78, 71, 13, 10, 26, 10>>   # PNG file signature\n\n  def parse(<<@magic::binary, width::32, height::32, _rest::binary>>) do\n    {:ok, width, height}\n  end\n\n  def parse(_other), do: {:error, :not_a_png}\nend\n\nPNG.parse(<<\n  137, 80, 78, 71, 13, 10, 26, 10,   # signature\n  0, 0, 8, 0,                        # width  = 2048\n  0, 0, 4, 80                        # height = 1104\n>>)\n|> IO.inspect()   #=> {:ok, 2048, 1104}\n\nPNG.parse(\"not a png\")\n|> IO.inspect(label: \"no match\")",
              "note": "`::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."
            }
          ]
        }
      ]
    },
    {
      "id": "functions",
      "title": "Functions",
      "icon": "function-square",
      "blurb": "Named functions with pattern-matched clauses, guarded branches, anonymous functions and the capture operator — functions are values you can pass anywhere.",
      "entries": [
        {
          "id": "functions-named",
          "title": "Named functions: def & defp",
          "summary": "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`.",
          "keywords": [
            "def",
            "defp",
            "clauses",
            "arity",
            "module",
            "pattern matching"
          ],
          "since": null,
          "exercises": "009–013",
          "blocks": [
            {
              "kind": "text",
              "content": "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."
            },
            {
              "kind": "code",
              "title": "Clauses, privacy, arity",
              "code": "defmodule Text do\n  # public — callable from other modules as Text.format/2\n  def format(lines, width) do\n    lines\n    |> Enum.join(\" \")\n    |> wrap(width)\n  end\n\n  # private — visible only inside this module\n  defp wrap(string, width) when byte_size(string) <= width, do: string\n  defp wrap(string, width), do: String.slice(string, 0, width) <> \"…\"\nend\n\nText.format([\"elixir\", \"rocks\"], 40)\n|> IO.inspect()   #=> \"elixir rocks\"",
              "note": "`format/2` and `format/3` would be two different functions — adding a parameter changes the arity and therefore the identity."
            },
            {
              "kind": "list",
              "items": [
                "**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"
              ]
            }
          ]
        },
        {
          "id": "functions-default-args",
          "title": "Default arguments",
          "summary": "The `\\\\` operator (two backslashes) gives a parameter a default and generates one exported function per arity: `join/1` falls back to `join/2`.",
          "keywords": [
            "default arguments",
            "backslash",
            "arity",
            "optional",
            "function head"
          ],
          "since": null,
          "exercises": "009–013",
          "blocks": [
            {
              "kind": "text",
              "content": "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."
            },
            {
              "kind": "code",
              "title": "join/1 delegates to join/2",
              "code": "defmodule Join do\n  def join(list, sep \\\\ \", \"), do: Enum.join(list, sep)\nend\n\nJoin.join([\"a\", \"b\", \"c\"])\n|> IO.inspect(label: \"join/1\")\n\nJoin.join([\"a\", \"b\", \"c\"], \" + \")\n|> IO.inspect(label: \"join/2\")"
            },
            {
              "kind": "warn",
              "title": "Defaults + multiple clauses need a head",
              "content": "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."
            }
          ]
        },
        {
          "id": "functions-guards",
          "title": "Guards: when",
          "summary": "Guards are boolean checks after a pattern: `when` filters clauses, `defguard` composes your own, and since v1.20 they also feed the type checker.",
          "keywords": [
            "guard",
            "when",
            "defguard",
            "is_integer",
            "is_binary",
            "is_map_key",
            "type inference"
          ],
          "since": "v1.20",
          "exercises": "014–018",
          "blocks": [
            {
              "kind": "text",
              "content": "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."
            },
            {
              "kind": "code",
              "title": "Built-in and custom guards",
              "code": "defmodule Access do\n  # defguard composes guards into a reusable name\n  defguard is_adult(age) when is_integer(age) and age >= 18\n\n  def check(%{age: age}) when is_adult(age), do: :allowed\n  def check(%{age: age}) when is_integer(age), do: :minor\n  def check(_person), do: :unknown\nend\n\nAccess.check(%{age: 21})   #=> :allowed\nAccess.check(%{age: 9})    #=> :minor\nAccess.check(%{})          #=> :unknown\n\nIO.inspect([\n  Access.check(%{age: 21}),\n  Access.check(%{age: 9}),\n  Access.check(%{}),\n])"
            },
            {
              "kind": "list",
              "items": [
                "**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"
              ]
            }
          ]
        },
        {
          "id": "functions-anonymous",
          "title": "Anonymous functions",
          "summary": "`fn args -> body end` creates a function value, called with a dot. They support multiple clauses, guards, and close over variables by value.",
          "keywords": [
            "fn",
            "anonymous",
            "closure",
            "dot call",
            "immutable capture"
          ],
          "since": null,
          "exercises": "009–013",
          "blocks": [
            {
              "kind": "text",
              "content": "Anonymous functions are first-class values — store them, pass them, return them. The call syntax is `fun.(args)`: the dot makes the call explicit."
            },
            {
              "kind": "code",
              "title": "fn, dot calls, closures",
              "code": "double = fn x -> x * 2 end\ndouble.(4)          #=> 8 — call with a DOT\n\n# multiple clauses, matched top-down, guards allowed\nclassify = fn\n  x when x < 0 -> :negative\n  0 -> :zero\n  _x -> :positive\nend\nclassify.(-5)       #=> :negative\n\n# closures capture by value — data is immutable\nn = 10\nadd_n = fn x -> x + n end\nn = 0               # rebinding outside does not change the closure\nadd_n.(5)           #=> 15\n\nIO.inspect([double.(4), classify.(-5), add_n.(5)])",
              "note": "Forgetting the dot raises UndefinedFunctionError — `double(4)` would look for a named function `double/1`."
            }
          ]
        },
        {
          "id": "functions-capture",
          "title": "The capture operator: &",
          "summary": "`&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.",
          "keywords": [
            "capture",
            "ampersand",
            "placeholder",
            "partial application",
            "erlang interop"
          ],
          "since": "v1.19",
          "exercises": "009–013",
          "blocks": [
            {
              "kind": "text",
              "content": "`&` 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."
            },
            {
              "kind": "code",
              "title": "Four ways to capture",
              "code": "Enum.map([1, 2, 3], &(&1 * 10))       #=> [10, 20, 30]\nadd = &(&1 + &2)                      # &2 = second argument\nadd.(3, 4)                            #=> 7\n\nEnum.reduce([1, 2, 3], 0, &+/2)       # capture operators too\nEnum.map([\"a\", :b, 2], &to_string/1)  #=> [\"a\", \"b\", \"2\"]\n\nupcase = &String.upcase/1             # named function as value\nremote = &:erlang.node/0              # Erlang functions included\nupcase.(\"elixir\")                     #=> \"ELIXIR\"\n\nIO.inspect([\n  Enum.map([1, 2, 3], &(&1 * 10)),\n  add.(3, 4),\n  Enum.reduce([1, 2, 3], 0, &+/2),\n  Enum.map([\"a\", :b, 2], &to_string/1),\n  upcase.(\"elixir\"),\n])",
              "note": "**Since v1.19** the compiler propagates types through captures — `Enum.map(names, &String.upcase/1)` is fully type-checked end to end."
            }
          ]
        },
        {
          "id": "functions-higher-order",
          "title": "Higher-order functions & then/2",
          "summary": "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.",
          "keywords": [
            "higher-order",
            "then",
            "pipeline",
            "closure factory",
            "composition"
          ],
          "since": null,
          "exercises": "009–013",
          "blocks": [
            {
              "kind": "text",
              "content": "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."
            },
            {
              "kind": "code",
              "title": "Functions as arguments and factories",
              "code": "transform = fn fun, x -> fun.(fun.(x)) end\ntransform.(&(&1 * 3), 2)          #=> 18\n\n# then/2 threads a value through a single function\nport =\n  System.get_env(\"ELIXIR_CHEATS_PORT\")   # unset -> nil\n  |> then(fn\n    nil -> \"4000\"\n    port -> port\n  end)\n  |> String.to_integer()\n\n# functions building functions — closures as factories\nprefix = fn tag -> fn msg -> \"[#{tag}] #{msg}\" end end\nlog = prefix.(\"info\")\nlog.(\"started\")                   #=> \"[info] started\"\n\nIO.inspect(port)\nIO.inspect(log.(\"started\"))"
            },
            {
              "kind": "list",
              "items": [
                "`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"
              ]
            }
          ]
        }
      ]
    },
    {
      "id": "control-flow",
      "title": "Control Flow",
      "icon": "git-branch",
      "blurb": "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.",
      "entries": [
        {
          "id": "control-flow-if-cond",
          "title": "if / else / unless / cond",
          "summary": "`if` handles one condition, `cond` tries many boolean branches — and both are expressions you can assign. `unless` is soft-deprecated since v1.17.",
          "keywords": [
            "if",
            "else",
            "unless",
            "cond",
            "expression",
            "truthiness"
          ],
          "since": "v1.17",
          "exercises": "019–022",
          "blocks": [
            {
              "kind": "text",
              "content": "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."
            },
            {
              "kind": "code",
              "title": "Branching as an expression",
              "code": "n = 7\n\nparity =\n  if rem(n, 2) == 0 do\n    \"even\"\n  else\n    \"odd\"\n  end\n#=> \"odd\"\n\ngrade =\n  cond do\n    n >= 90 -> :a\n    n >= 80 -> :b\n    n >= 70 -> :c\n    true -> :f            # true acts as the catch-all\n  end\n#=> :f\n\nIO.inspect([parity, grade])"
            },
            {
              "kind": "compare",
              "left": {
                "title": "unless — soft-deprecated",
                "code": "# warns since v1.17\nunless Enum.empty?(errors) do\n  IO.puts(\"found problems!\")\nend"
              },
              "right": {
                "title": "if not — idiomatic",
                "code": "if not Enum.empty?(errors) do\n  IO.puts(\"found problems!\")\nend"
              },
              "note": "`unless` emits a deprecation warning since v1.17; the migration is mechanical — `unless x` becomes `if not x`."
            }
          ]
        },
        {
          "id": "control-flow-case",
          "title": "case — match & dispatch",
          "summary": "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.",
          "keywords": [
            "case",
            "pattern matching",
            "guards",
            "CaseClauseError",
            "type narrowing"
          ],
          "since": "v1.20",
          "exercises": "019–022",
          "blocks": [
            {
              "kind": "text",
              "content": "`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."
            },
            {
              "kind": "code",
              "title": "Clauses + guards, first match wins",
              "code": "resp = {201, %{id: 42}}\n\noutcome =\n  case resp do\n    {status, _body} when status in 200..299 -> {:ok, :created}\n    {404, _body} -> {:error, :not_found}\n    {status, _body} when status >= 500 -> {:error, :server}\n    _other -> {:error, :unknown}     # catch-all keeps this total\n  end\n#=> {:ok, :created}\n\nIO.inspect(outcome)"
            },
            {
              "kind": "code",
              "title": "Type narrowing across clauses (v1.20)",
              "code": "case System.get_env(\"ELIXIR_CHEATS_PORT\") do   # unset -> nil\n  nil -> 4000                      # nil is handled first...\n  port -> String.to_integer(port)  # ...so port is a binary here\nend\n|> IO.inspect()",
              "note": "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."
            },
            {
              "kind": "warn",
              "title": "No match = crash",
              "content": "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."
            }
          ]
        },
        {
          "id": "control-flow-with",
          "title": "with — the happy path",
          "summary": "Chain steps that each return `{:ok, value}`; `<-` pattern-matches and binds. The first non-matching step short-circuits — and returns its raw value.",
          "keywords": [
            "with",
            "happy path",
            "else",
            "chaining",
            "short-circuit"
          ],
          "since": null,
          "exercises": "019–022",
          "blocks": [
            {
              "kind": "text",
              "content": "`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."
            },
            {
              "kind": "code",
              "title": "Chain matches, normalize failures with else",
              "code": "defmodule Profile do\n  def city(map) do\n    with {:ok, user} <- Map.fetch(map, :user),\n         {:ok, address} <- Map.fetch(user, :address),\n         {:ok, city} <- Map.fetch(address, :city) do\n      {:ok, city}\n    else\n      :error -> {:error, :missing_key}   # Map.fetch fails with :error\n      other -> other                     # any other miss, passed through\n    end\n  end\nend\n\nProfile.city(%{user: %{address: %{city: \"Tokyo\"}}})\n#=> {:ok, \"Tokyo\"}\n\nIO.inspect(Profile.city(%{user: %{address: %{city: \"Tokyo\"}}}))\nIO.inspect(Profile.city(%{}))   # the else branch normalises the miss"
            },
            {
              "kind": "warn",
              "title": "Bare failure values — the with gotcha",
              "content": "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."
            }
          ]
        },
        {
          "id": "control-flow-for",
          "title": "for comprehensions",
          "summary": "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.",
          "keywords": [
            "for",
            "comprehension",
            "generator",
            "filter",
            "into",
            "reduce"
          ],
          "since": "v1.19",
          "exercises": "041–043",
          "blocks": [
            {
              "kind": "code",
              "title": "Generators, filters, into:, reduce:",
              "code": "# generator + filter\nfor n <- 1..5, rem(n, 2) == 1, do: n * n\n#=> [1, 9, 25]\n\n# multiple generators nest like loops\nfor x <- [1, 2], y <- ~w(a b), do: {x, y}\n#=> [{1, \"a\"}, {1, \"b\"}, {2, \"a\"}, {2, \"b\"}]\n\n# pattern filters skip non-matching elements\nfor {:ok, value} <- [{:ok, 1}, :error, {:ok, 3}], do: value\n#=> [1, 3]\n\n# into: collects into any collectable — binaries, MapSets, tuples...\nfor s <- [\"ab\", \"cd\"], into: \"\", do: s\n#=> \"abcd\"\n\nfor x <- [1, 2, 2, 3], into: MapSet.new(), do: x\n#=> MapSet.new([1, 2, 3])\n\n# reduce: turns the comprehension into an accumulation\nfor n <- 1..100, reduce: 0 do\n  acc -> if rem(n, 3) == 0, do: acc + n, else: acc\nend\n#=> 1683\n\nIO.inspect(for n <- 1..5, rem(n, 2) == 1, do: n * n)\nIO.inspect(for x <- [1, 2], y <- ~w(a b), do: {x, y})\nIO.inspect(for {:ok, value} <- [{:ok, 1}, :error, {:ok, 3}], do: value)\nIO.inspect(for s <- [\"ab\", \"cd\"], into: \"\", do: s)\nIO.inspect(for x <- [1, 2, 2, 3], into: MapSet.new(), do: x)\n\nIO.inspect(\n  for n <- 1..100, reduce: 0 do\n    acc -> if rem(n, 3) == 0, do: acc + n, else: acc\n  end\n)"
            },
            {
              "kind": "list",
              "items": [
                "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"
              ]
            }
          ]
        },
        {
          "id": "control-flow-decision",
          "title": "Which construct when?",
          "summary": "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.",
          "keywords": [
            "decision",
            "if vs case",
            "cond",
            "with",
            "for",
            "choosing"
          ],
          "since": null,
          "exercises": "019–022",
          "blocks": [
            {
              "kind": "text",
              "content": "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`."
            },
            {
              "kind": "table",
              "headers": [
                "Construct",
                "Use when",
                "Example hint"
              ],
              "rows": [
                [
                  "`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"
                ]
              ]
            }
          ]
        }
      ]
    },
    {
      "id": "enum-streams",
      "title": "Enum & Streams",
      "icon": "list-filter",
      "blurb": "The workhorse module, the pipe operator that glues it together, and the lazy Stream for when the data gets big — or infinite.",
      "entries": [
        {
          "id": "enum-streams-essentials",
          "title": "Enum essentials",
          "summary": "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.",
          "keywords": [
            "enum",
            "map",
            "filter",
            "reduce",
            "sort",
            "group_by",
            "min_max",
            "frequencies"
          ],
          "since": "v1.20",
          "exercises": "028–035",
          "blocks": [
            {
              "kind": "text",
              "content": "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."
            },
            {
              "kind": "code",
              "title": "The core iteration set",
              "code": "orders = [\n  %{id: 1, total: 30, user: \"ada\"},\n  %{id: 2, total: 120, user: \"grace\"},\n  %{id: 3, total: 60, user: \"linus\"}\n]\n\nEnum.map(orders, & &1.total)              #=> [30, 120, 60]\nEnum.filter(orders, &(&1.total > 50))     #=> orders 2 and 3\nEnum.find(orders, &(&1.total > 100))      #=> %{id: 2, ...}\nEnum.count(orders, &(&1.total > 50))      #=> 2\nEnum.any?(orders, &(&1.total > 100))      #=> true\nEnum.all?(orders, &(&1.total > 0))        #=> true\nEnum.reduce(orders, 0, &(&1.total + &2))  #=> 210\n\nIO.inspect(Enum.map(orders, & &1.total))\nIO.inspect(Enum.count(orders, &(&1.total > 50)))\nIO.inspect(Enum.find(orders, &(&1.total > 100)))\nIO.inspect(Enum.reduce(orders, 0, &(&1.total + &2)))"
            },
            {
              "kind": "code",
              "title": "Shaping and splitting",
              "code": "orders = [\n  %{id: 1, total: 30, user: \"ada\"},\n  %{id: 2, total: 120, user: \"grace\"},\n  %{id: 3, total: 60, user: \"linus\"}\n]\n\nEnum.sort_by(orders, & &1.total, :desc)   #=> biggest first\nEnum.group_by(orders, & &1.user)          #=> %{\"ada\" => [...], ...}\nEnum.split_with(orders, &(&1.total > 50)) #=> {big, small}\nEnum.chunk_every([1, 2, 3, 4, 5], 2)      #=> [[1, 2], [3, 4], [5]]\nEnum.frequencies(~w(a b a c))             #=> %{\"a\" => 2, \"b\" => 1, \"c\" => 1}\n\n# min_by/2 and max_by/2 pick by a key; min_max/1 works on ordered values\nEnum.min_by(orders, & &1.total)         #=> the cheapest order\nEnum.max_by(orders, & &1.total)         #=> the priciest order\nEnum.min_max([10, 20, 30])              #=> {10, 30}\nEnum.sum([10, 20, 30])                    #=> 60\n\nIO.inspect(Enum.map(Enum.sort_by(orders, & &1.total, :desc), & &1.id))\nIO.inspect(Map.keys(Enum.group_by(orders, & &1.user)))\nIO.inspect(Enum.chunk_every([1, 2, 3, 4, 5], 2))\nIO.inspect(Enum.frequencies(~w(a b a c)))\nIO.inspect(Enum.map([Enum.min_by(orders, & &1.total), Enum.max_by(orders, & &1.total)], & &1.id))\nIO.inspect(Enum.min_max([10, 20, 30]))\nIO.inspect(Enum.sum([10, 20, 30]))"
            }
          ]
        },
        {
          "id": "enum-streams-pipe",
          "title": "The pipe operator: |>",
          "summary": "`|>` 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.",
          "keywords": [
            "pipe",
            "pipeline",
            "then",
            "dbg",
            "first argument"
          ],
          "since": null,
          "exercises": "028–035",
          "blocks": [
            {
              "kind": "text",
              "content": "`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."
            },
            {
              "kind": "code",
              "title": "A pipeline reads like the problem",
              "code": "\"data.csv\"\n|> File.read!()\n|> String.split(\"\\n\")\n|> Enum.map(&String.trim/1)\n|> Enum.reject(&(&1 == \"\"))\n|> Enum.count()\n|> IO.inspect(label: \"lines\")",
              "runFiles": {
                "data.csv": "id,total,user\n1,30,ada\n2,120,grace\n3,60,linus\n"
              },
              "note": "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."
            },
            {
              "kind": "code",
              "title": "Debugging pipes and non-first-arg steps",
              "code": "[3, 1, 2]\n|> Enum.sort()\n|> dbg()          # since v1.20: prints EVERY step with its result\n|> Enum.sum()\n#=> 6\n\nport =\n  System.get_env(\"ELIXIR_CHEATS_PORT\")   # unset -> nil\n  |> then(fn\n    nil -> \"4000\"\n    port -> port\n  end)\n  |> String.to_integer()\n\nIO.inspect(port)",
              "note": "`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."
            }
          ]
        },
        {
          "id": "enum-streams-streams",
          "title": "Streams — lazy & composable",
          "summary": "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.",
          "keywords": [
            "stream",
            "lazy",
            "file",
            "memory",
            "infinite",
            "cycle"
          ],
          "since": null,
          "exercises": "048–050",
          "blocks": [
            {
              "kind": "code",
              "title": "Laziness and constant-memory file processing",
              "code": "# lazy: nothing runs until Enum.take pulls values\n1..1_000_000_000\n|> Stream.map(&(&1 * 3))\n|> Stream.filter(&(rem(&1, 2) == 0))\n|> Enum.take(3)\n#=> [6, 12, 18]\n\n# stream a large file line by line — constant memory\nFile.stream!(\"logs/app.log\")\n|> Stream.map(&String.trim_trailing(&1, \"\\n\"))\n|> Stream.filter(&String.contains?(&1, \"ERROR\"))\n|> Enum.take(5)\n|> IO.inspect(label: \"errors\")",
              "runFiles": {
                "logs/app.log": "INFO boot\nERROR timeout after 5s\nINFO retry\nERROR connection refused\nINFO up\n"
              }
            },
            {
              "kind": "warn",
              "title": "Streams do nothing until forced",
              "content": "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."
            },
            {
              "kind": "list",
              "items": [
                "**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"
              ]
            }
          ]
        },
        {
          "id": "enum-streams-comprehensions-vs-enum",
          "title": "Comprehensions vs Enum pipelines",
          "summary": "Two styles, one result: `for` walks the data once and reads like a spec; Enum pipelines compose eager steps. Pick whichever reads better.",
          "keywords": [
            "comprehension",
            "pipeline",
            "style",
            "single pass",
            "filter map"
          ],
          "since": null,
          "exercises": "041–043",
          "blocks": [
            {
              "kind": "text",
              "content": "`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."
            },
            {
              "kind": "compare",
              "left": {
                "title": "for — single pass",
                "code": "squares =\n  for n <- 1..10, rem(n, 2) == 0 do\n    n * n\n  end\n#=> [4, 16, 36, 64, 100]"
              },
              "right": {
                "title": "Enum — composable steps",
                "code": "squares =\n  1..10\n  |> Enum.filter(&(rem(&1, 2) == 0))\n  |> Enum.map(&(&1 * &1))\n#=> [4, 16, 36, 64, 100]"
              },
              "note": "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."
            }
          ]
        },
        {
          "id": "enum-streams-table",
          "title": "Essential Enum functions",
          "summary": "The daily-driver table: what each function does and what comes back. Everything here works on any Enumerable and returns new data.",
          "keywords": [
            "cheat table",
            "reference",
            "enum functions",
            "returns"
          ],
          "since": null,
          "exercises": "028–035",
          "blocks": [
            {
              "kind": "text",
              "content": "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."
            },
            {
              "kind": "table",
              "headers": [
                "Function",
                "What it does",
                "Returns"
              ],
              "rows": [
                [
                  "`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"
                ]
              ]
            }
          ]
        }
      ]
    },
    {
      "id": "recursion",
      "title": "Recursion",
      "icon": "repeat",
      "blurb": "No while, no loop statements — pattern-matched clauses are the loop, accumulators make tail calls constant-stack, and Enum.reduce generalizes it all.",
      "entries": [
        {
          "id": "recursion-body",
          "title": "Body recursion over lists",
          "summary": "There are no loops in Elixir: a recursion is clauses — a base case for `[]` and a recursive case for `[head | tail]`.",
          "keywords": [
            "recursion",
            "base case",
            "head tail",
            "list pattern",
            "no loops"
          ],
          "since": null,
          "exercises": "036–040",
          "blocks": [
            {
              "kind": "text",
              "content": "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."
            },
            {
              "kind": "code",
              "title": "Sum and map, by hand",
              "code": "defmodule MyList do\n  # base case: empty list — stops the recursion\n  def sum([]), do: 0\n\n  # recursive case: peel the head, recurse on the tail\n  def sum([head | tail]), do: head + sum(tail)\n\n  def square([]), do: []\n  def square([head | tail]), do: [head * head | square(tail)]\nend\n\nMyList.sum([1, 2, 3, 4])      #=> 10\nMyList.square([1, 2, 3, 4])   #=> [1, 4, 9, 16]\n\nIO.inspect(MyList.sum([1, 2, 3, 4]))\nIO.inspect(MyList.square([1, 2, 3, 4]))"
            },
            {
              "kind": "warn",
              "title": "Missing base case = infinite recursion",
              "content": "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."
            }
          ]
        },
        {
          "id": "recursion-tail-calls",
          "title": "Tail calls & accumulators",
          "summary": "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.",
          "keywords": [
            "tail call",
            "accumulator",
            "stack",
            "optimization",
            "enum.reverse"
          ],
          "since": null,
          "exercises": "036–040",
          "blocks": [
            {
              "kind": "text",
              "content": "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."
            },
            {
              "kind": "compare",
              "left": {
                "title": "Body recursion — stack grows",
                "code": "defmodule Body do\n  def len([]), do: 0\n  def len([_ | tail]), do: 1 + len(tail)\nend\n\n# 1 + (1 + (1 + ...)) — every frame waits for\n# the next call, so the stack grows with the list"
              },
              "right": {
                "title": "Tail recursion — constant stack",
                "code": "defmodule Tail do\n  def len(list), do: go(list, 0)\n\n  defp go([], acc), do: acc\n  defp go([_ | tail], acc), do: go(tail, acc + 1)\nend\n\n# acc carries the result; the recursive call IS\n# the last operation, so BEAM reuses the frame"
              }
            },
            {
              "kind": "code",
              "title": "The Enum.reverse/1 idiom",
              "code": "defmodule Doubler do\n  def run(list), do: go(list, [])\n\n  defp go([], acc), do: Enum.reverse(acc)      # restore order\n  defp go([head | tail], acc), do: go(tail, [head * 2 | acc])\nend\n\nDoubler.run([1, 2, 3])  #=> [2, 4, 6]\n\nIO.inspect(Doubler.run([1, 2, 3]))",
              "note": "Prepending to the accumulator is O(1); appending is O(n) — so accumulate reversed and finish with the classic `Enum.reverse/1`."
            }
          ]
        },
        {
          "id": "recursion-reduce",
          "title": "reduce as universal recursion",
          "summary": "An accumulator threaded through clauses is exactly Enum.reduce — map, filter, sum and friends are all special cases of it.",
          "keywords": [
            "reduce",
            "fold",
            "accumulator",
            "universal recursion",
            "enum"
          ],
          "since": null,
          "exercises": "028–035",
          "blocks": [
            {
              "kind": "text",
              "content": "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."
            },
            {
              "kind": "code",
              "title": "The same recursions, declaratively",
              "code": "Enum.reduce([1, 2, 3, 4], 0, &+/2)\n#=> 10 — the recursive sum, one line\n\nsquares =\n  [1, 2, 3, 4]\n  |> Enum.reduce([], &[&1 * &1 | &2])\n  |> Enum.reverse()\n#=> [1, 4, 9, 16]\n\nIO.inspect(Enum.reduce([1, 2, 3, 4], 0, &+/2))\nIO.inspect([1, 2, 3, 4] |> Enum.reduce([], &[&1 * &1 | &2]) |> Enum.reverse())",
              "note": "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."
            }
          ]
        },
        {
          "id": "recursion-shapes",
          "title": "Recursion over multiple shapes",
          "summary": "Clauses can dispatch on several shapes at once — multiple base cases, nested structures, guards — but every path needs a terminating clause.",
          "keywords": [
            "multiple base cases",
            "nested",
            "flatten",
            "guards",
            "dispatch"
          ],
          "since": null,
          "exercises": "036–040",
          "blocks": [
            {
              "kind": "text",
              "content": "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."
            },
            {
              "kind": "code",
              "title": "Two base cases + nested shapes",
              "code": "defmodule Nested do\n  # multiple base cases\n  def fib(0), do: 0\n  def fib(1), do: 1\n  def fib(n) when is_integer(n) and n > 1, do: fib(n - 1) + fib(n - 2)\n\n  # recursion over nested shapes\n  def flatten([]), do: []\n  def flatten([head | tail]) when is_list(head),\n    do: flatten(head) ++ flatten(tail)\n  def flatten([head | tail]), do: [head | flatten(tail)]\nend\n\nNested.flatten([1, [2, [3, 4]], 5])  #=> [1, 2, 3, 4, 5]\n\nIO.inspect(Enum.map(0..10, &Nested.fib/1))\nIO.inspect(Nested.flatten([1, [2, [3, 4]], 5]))"
            }
          ]
        }
      ]
    },
    {
      "id": "errors",
      "title": "Errors & Exceptions",
      "icon": "triangle-alert",
      "blurb": "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.",
      "entries": [
        {
          "id": "errors-ok-error",
          "title": "Error values vs bang functions",
          "summary": "Expected failures come back as `{:error, reason}`; bang (`!`) variants raise instead. `try/rescue` is for boundary code — the anatomy comes in the next entry.",
          "keywords": [
            "ok error tuple",
            "bang",
            "fetch",
            "File.read",
            "convention"
          ],
          "since": null,
          "exercises": "060–064",
          "blocks": [
            {
              "kind": "text",
              "content": "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.)"
            },
            {
              "kind": "code",
              "title": "Two failure styles side by side",
              "code": "user = %{name: \"Ada\", role: :admin}\n\nMap.fetch(user, :age)      #=> :error — absence is a value\nMap.fetch(user, :name)     #=> {:ok, \"Ada\"}\n\nMap.fetch!(user, :role)    #=> :admin — bang raises when missing\nFile.read(\"config.toml\")   #=> {:ok, data} | {:error, :enoent}\nFile.read!(\"config.toml\")  #=> data, or ** (File.Error)\n\nIO.inspect(Map.fetch(user, :age))\nIO.inspect(Map.fetch(user, :name))\nIO.inspect(Map.fetch!(user, :role))\nIO.inspect(File.read(\"config.toml\"))\n\ntry do\n  File.read!(\"missing.toml\")\nrescue\n  e -> IO.puts(\"rescued: \" <> Exception.message(e))\nend",
              "runFiles": {
                "config.toml": "theme = \"dark\"\n"
              }
            },
            {
              "kind": "list",
              "items": [
                "**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"
              ]
            }
          ]
        },
        {
          "id": "errors-raise-rescue",
          "title": "raise, defexception & try anatomy",
          "summary": "`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.",
          "keywords": [
            "raise",
            "defexception",
            "try",
            "rescue",
            "else",
            "after"
          ],
          "since": null,
          "exercises": "060–064",
          "blocks": [
            {
              "kind": "text",
              "content": "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."
            },
            {
              "kind": "code",
              "title": "Custom exceptions",
              "code": "defmodule PaymentError do\n  defexception [:message, :reason]\n\n  @impl true\n  def message(%__MODULE__{message: msg, reason: reason}) do\n    \"#{msg} (reason: #{inspect(reason)})\"\n  end\nend\n\n# raise \"simple message\"                # RuntimeError\n# raise PaymentError,\n#   message: \"declined\",\n#   reason: :insufficient_funds\n#=> ** (PaymentError) declined (reason: :insufficient_funds)\n\ntry do\n  raise \"simple message\"\nrescue\n  e -> IO.puts(\"RuntimeError: \" <> Exception.message(e))\nend\n\ntry do\n  raise PaymentError, message: \"declined\", reason: :insufficient_funds\nrescue\n  e in PaymentError -> IO.puts(\"PaymentError: \" <> Exception.message(e))\nend"
            },
            {
              "kind": "code",
              "title": "try / rescue / else / after — full anatomy",
              "code": "result =\n  try do\n    {:ok, JSON.decode!(File.read!(\"settings.json\"))}\n  rescue\n    e in File.Error -> {:error, {:file, e.reason}}\n    e in JSON.DecodeError -> {:error, {:json, Exception.message(e)}}\n  else\n    {:ok, data} -> {:ok, data[\"theme\"]}\n  after\n    IO.puts(\"attempt finished\")     # always runs — cleanup\n  end\n\nIO.inspect(result)",
              "runFiles": {
                "settings.json": "{\"theme\": \"dark\"}\n"
              },
              "note": "Order is do → rescue → else → after. `else` pattern-matches the value when nothing raised; `after` runs on success, raise, throw and exit alike."
            }
          ]
        },
        {
          "id": "errors-throw-catch-exit",
          "title": "throw / catch & exit",
          "summary": "`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.",
          "keywords": [
            "throw",
            "catch",
            "exit",
            "nocatch",
            "trap_exit",
            "process"
          ],
          "since": null,
          "exercises": "060–064",
          "blocks": [
            {
              "kind": "text",
              "content": "`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."
            },
            {
              "kind": "code",
              "title": "catch matches the thrown value; exit ends a process",
              "code": "# catch pattern-matches the thrown value\ntry do\n  throw(:early)\ncatch\n  :early -> :caught_it\nend\n#=> :caught_it\n\n# exit/1 ends the current process on purpose\nProcess.flag(:trap_exit, true)   # parent survives; gets :EXIT messages\npid = spawn_link(fn -> exit(:boom) end)\n\nreceive do\n  {:EXIT, ^pid, :boom} -> :child_exited\nend\n#=> :child_exited\n\nIO.inspect(\n  try do\n    throw(:early)\n  catch\n    :early -> :caught_it\n  end\n)\n\nProcess.flag(:trap_exit, true)   # parent survives; gets :EXIT messages\npid = spawn_link(fn -> exit(:boom) end)\n\nreceive do\n  {:EXIT, ^pid, :boom} -> IO.puts(\"child_exited\")\nend"
            },
            {
              "kind": "compare",
              "left": {
                "title": "OK — return error values",
                "code": "def parse_age(params) do\n  case Integer.parse(params[\"age\"]) do\n    {age, \"\"} -> {:ok, age}\n    _ -> {:error, :invalid_age}\n  end\nend"
              },
              "right": {
                "title": "Anti-pattern — throw for control flow",
                "code": "def parse_age(params) do\n  case Integer.parse(params[\"age\"]) do\n    {age, \"\"} -> {:ok, age}\n    _ -> throw({:error, :invalid_age})  # every caller must catch\n  end\nend"
              },
              "note": "`throw` skips normal returns: one forgotten `catch` wrapper turns a handled error into a nocatch crash. Keep `{:error, reason}` for expected failures."
            },
            {
              "kind": "warn",
              "title": "throw and exit are framework territory",
              "content": "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."
            }
          ]
        },
        {
          "id": "errors-let-it-crash",
          "title": "The let-it-crash philosophy",
          "summary": "Expected failures are values, bugs are crashes: let the process die and let a supervisor restart it in a known-good state.",
          "keywords": [
            "let it crash",
            "supervisor",
            "restart",
            "blast radius",
            "philosophy"
          ],
          "since": null,
          "exercises": "060–064",
          "blocks": [
            {
              "kind": "text",
              "content": "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."
            },
            {
              "kind": "list",
              "items": [
                "**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"
              ]
            }
          ]
        },
        {
          "id": "errors-type-checker",
          "title": "v1.20 type checker × error handling",
          "summary": "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.",
          "keywords": [
            "type checker",
            "gradual typing",
            "dead code",
            "disjoint",
            "Map.fetch!",
            "narrowing"
          ],
          "since": "v1.20",
          "exercises": "060–064",
          "blocks": [
            {
              "kind": "text",
              "content": "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."
            },
            {
              "kind": "code",
              "title": "Provable bugs, found for free",
              "code": "defmodule Users do\n  def name!(user) do\n    Map.fetch!(user, :name)\n  end\n\n  def role(user) do\n    case Map.fetch(user, :role) do\n      {:ok, role} -> role\n      :error -> :guest\n      other -> other               # v1.20: dead clause warning\n    end\n  end\nend\n\n# since v1.20 the compiler proves this call can only raise:\n#   Users.name!(%{})   # warning: %{} can never have :name\nUsers.name!(%{name: \"Ada\"})        #=> \"Ada\"\n\nIO.inspect(Users.role(%{role: :admin}))\nIO.inspect(Users.name!(%{name: \"Ada\"}))\n\ntry do\n  Users.name!(%{})\nrescue\n  e -> IO.puts(\"rescued: \" <> Exception.message(e))\nend",
              "note": "`Map.fetch!` needs a map with the `:name` key; `%{}` is disjoint from that type, so the call is flagged before it ever runs."
            },
            {
              "kind": "list",
              "items": [
                "**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`"
              ]
            }
          ]
        }
      ]
    },
    {
      "id": "processes-otp",
      "title": "Processes & OTP",
      "icon": "boxes",
      "blurb": "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.",
      "entries": [
        {
          "id": "processes-otp-spawn-messages",
          "title": "spawn, send & receive",
          "summary": "Processes are spawned from functions, exchange **immutable messages**, and each has a mailbox. State lives in a recursive loop — the argument **is** the state.",
          "keywords": [
            "spawn",
            "send",
            "receive",
            "mailbox",
            "pid",
            "self",
            "message passing",
            "timeout"
          ],
          "since": null,
          "exercises": null,
          "blocks": [
            {
              "kind": "code",
              "title": "Spawn a process and talk to it",
              "code": "parent = self()\n\n_pid = spawn(fn ->\n  receive do                    # wait for a message in the mailbox\n    {:hello, from} -> send(from, :hi)\n  after\n    5_000 -> :timeout           # give up after 5 seconds\n  end\nend)\n\nsend(_pid, {:hello, parent})    # the child receives it and answers the sender\nreceive do\n  :hi -> :got_a_reply\nend\n|> IO.inspect()",
              "note": "`send/2` never blocks — messages are copied into the mailbox. `receive` scans the mailbox top-down; unhandled messages stay in the mailbox."
            },
            {
              "kind": "code",
              "title": "The state-in-argument loop pattern",
              "code": "defmodule Counter do\n  def loop(count) do            # the argument is the process state\n    receive do\n      {:incr, by} -> loop(count + by)\n      :stop -> :ok              # no recursive call = process exits\n      {:get, caller} ->\n        send(caller, {:count, count})\n        loop(count)\n    end\n  end\nend\n\ncounter = spawn(Counter, :loop, [0])\nsend(counter, {:incr, 2})\nsend(counter, {:get, self()})\nreceive do {:count, n} -> n end  #=> 2\n|> IO.inspect()",
              "note": "Processes are extremely cheap: heaps of hundreds of thousands are normal, each with its own garbage collection. Crashing one never corrupts another."
            }
          ]
        },
        {
          "id": "processes-otp-links-monitors",
          "title": "Links, monitors & exit signals",
          "summary": "`spawn_link` ties two processes together (both die together); `Process.monitor/1` gets a `:DOWN` message instead. `trap_exit` converts exit signals into messages.",
          "keywords": [
            "spawn_link",
            "link",
            "monitor",
            "trap_exit",
            "exit signal",
            "down",
            "crash"
          ],
          "since": null,
          "exercises": null,
          "blocks": [
            {
              "kind": "code",
              "title": "Link vs monitor",
              "code": "# a link is bidirectional: a crash in the child kills this process too\n# spawn_link(fn -> raise \"boom\" end)\n\nProcess.flag(:trap_exit, true)   # convert exit signals to messages\nchild = spawn_link(fn -> Process.sleep(:infinity) end)\nProcess.exit(child, :kill)\nreceive do\n  {:EXIT, ^child, reason} -> reason\nend\n|> IO.inspect(label: \"exit\")\n\nref = Process.monitor(child)     # monitor is one-way and stackable\nreceive do\n  {:DOWN, ^ref, :process, _pid, reason} -> reason\nend\n|> IO.inspect(label: \"down\")"
            },
            {
              "kind": "list",
              "items": [
                "**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"
              ]
            }
          ]
        },
        {
          "id": "processes-otp-task-agent",
          "title": "Task & Agent — lightweight OTP",
          "summary": "`Task` = one-off async computation with error propagation; `Agent` = a tiny state holder. Both are GenServers under the hood — no hand-written receive loops.",
          "keywords": [
            "task",
            "async",
            "await",
            "async_stream",
            "agent",
            "state",
            "concurrency"
          ],
          "since": null,
          "exercises": null,
          "blocks": [
            {
              "kind": "code",
              "title": "Task — async/await and concurrent streams",
              "code": "defmodule Work do\n  def heavy(n), do: n * n\nend\n\ntask = Task.async(fn -> Work.heavy(1) end)   # spawn + monitor + link\nresult = Task.await(task, 5_000)             # raises on crash or timeout\nresult\n|> IO.inspect(label: \"await\")\n\n# run one function over many items, bounded concurrency:\n1..10\n|> Task.async_stream(&Work.heavy(&1),\n  max_concurrency: System.schedulers_online(),\n  timeout: 10_000\n)\n|> Enum.to_list()                     # [{:ok, _} | {:exit, _}, ...]\n|> Enum.map(fn {:ok, value} -> value end)\n|> IO.inspect(label: \"stream\")",
              "note": "`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."
            },
            {
              "kind": "code",
              "title": "Agent — background state without boilerplate",
              "code": "{:ok, agent} = Agent.start_link(fn -> %{} end)\n\nAgent.update(agent, &Map.put(&1, :visits, 1))\nAgent.get(agent, &Map.get(&1, :visits))         #=> 1\n\nAgent.get_and_update(agent, fn state ->\n  # return {value_to_return, new_state}\n  {Map.get(state, :visits, 0),\n   Map.update(state, :visits, 1, &(&1 + 1))}\nend)\n|> IO.inspect(label: \"get_and_update\")\n\nAgent.get(agent, &Map.get(&1, :visits))\n|> IO.inspect(label: \"after\")",
              "note": "For state that must survive crashes or needs access policies, graduate to a GenServer supervised by your tree."
            }
          ]
        },
        {
          "id": "processes-otp-genserver",
          "title": "GenServer — the workhorse",
          "summary": "`use GenServer` + client API + callbacks: `init/1`, `handle_call/3` (sync), `handle_cast/2` (async). Register with a name so callers need no PID.",
          "keywords": [
            "genserver",
            "handle_call",
            "handle_cast",
            "init",
            "client api",
            "name registration",
            "behaviour"
          ],
          "since": null,
          "exercises": null,
          "blocks": [
            {
              "kind": "code",
              "title": "Minimal complete GenServer",
              "code": "defmodule Stack do\n  use GenServer\n\n  ## Client API — runs in the CALLER process\n  def start_link(initial),\n    do: GenServer.start_link(__MODULE__, initial, name: __MODULE__)\n\n  def push(item), do: GenServer.cast(__MODULE__, {:push, item})\n  def pop, do: GenServer.call(__MODULE__, :pop)\n\n  ## Callbacks — run in the SERVER process\n  @impl true\n  def init(initial), do: {:ok, initial}\n\n  @impl true\n  def handle_call(:pop, _from, [head | tail]), do: {:reply, head, tail}\n  def handle_call(:pop, _from, []), do: {:reply, nil, []}\n\n  @impl true\n  def handle_cast({:push, item}, state), do: {:noreply, [item | state]}\nend",
              "note": "`name: __MODULE__` registers a local name (one per node); `{:global, term}` or a `{:via, Registry, {...}}` tuple for wider scopes."
            },
            {
              "kind": "code",
              "title": "Using it",
              "code": "{:ok, _} = Stack.start_link([1, 2])\nStack.push(3)\nStack.pop()   #=> 3\nStack.pop()   #=> 2"
            },
            {
              "kind": "list",
              "items": [
                "**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"
              ]
            }
          ]
        },
        {
          "id": "processes-otp-supervisors",
          "title": "Supervisors — let it crash",
          "summary": "A Supervisor starts children, restarts them when they die, and gives up (taking the whole tree down) when restarts exceed the intensity limit.",
          "keywords": [
            "supervisor",
            "one_for_one",
            "strategy",
            "child spec",
            "restart intensity",
            "supervision tree"
          ],
          "since": null,
          "exercises": null,
          "blocks": [
            {
              "kind": "code",
              "title": "A supervision tree root",
              "code": "defmodule MyApp.Supervisor do\n  use Supervisor\n\n  def start_link(arg),\n    do: Supervisor.start_link(__MODULE__, arg, name: __MODULE__)\n\n  @impl true\n  def init(_arg) do\n    children = [\n      MyApp.Cache,                        # child_spec/1 from use GenServer\n      {MyApp.Worker, name: MyApp.Worker}  # module + start argument\n    ]\n\n    Supervisor.init(children,\n      strategy: :one_for_one,\n      max_restarts: 3,     # default: 3 restarts...\n      max_seconds: 5       # ...per 5 seconds, else the supervisor aborts\n    )\n  end\nend"
            },
            {
              "kind": "table",
              "headers": [
                "Strategy",
                "On child crash",
                "Use when"
              ],
              "rows": [
                [
                  "`: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"
                ]
              ]
            },
            {
              "kind": "warn",
              "title": "Restart intensity is a circuit breaker",
              "content": "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."
            }
          ]
        },
        {
          "id": "processes-otp-registry-ets",
          "title": "Registry & ETS — shared names and data",
          "summary": "`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.",
          "keywords": [
            "registry",
            "via tuple",
            "ets",
            "ordered_set",
            "dispatch",
            "pubsub",
            "named_table",
            "cache"
          ],
          "since": null,
          "exercises": null,
          "blocks": [
            {
              "kind": "code",
              "title": "Registry — names and dispatch",
              "code": "{:ok, _} = Registry.start_link(keys: :unique, name: MyApp.Registry)\n\n# name a process through the registry with a via tuple:\n{:ok, agent} =\n  Agent.start_link(fn -> 0 end,\n    name: {:via, Registry, {MyApp.Registry, {:counter, 42}}}\n  )\n\nRegistry.lookup(MyApp.Registry, {:counter, 42})   #=> [{pid, nil}]\n\nRegistry.register(MyApp.Registry, :topic, nil)    # unique: one subscriber\nRegistry.dispatch(MyApp.Registry, :topic, fn entries ->\n  for {pid, meta} <- entries, do: send(pid, {:event, meta})\nend)",
              "note": "Since v1.20, registries with `keys: :duplicate` are backed by an `:ordered_set` table — faster dispatch and lower memory for large fan-out."
            },
            {
              "kind": "code",
              "title": "ETS — shared key-value table",
              "code": "table = :ets.new(:cache, [:set, :public, read_concurrency: true])\n\n:ets.insert(table, {:ada, \"pioneer\"})\n:ets.lookup(table, :ada)           #=> [ada: \"pioneer\"]\n:ets.insert(table, {:ada, \"v2\"})   # insert overwrites — writes are atomic\n:ets.delete(table, :ada)\n\nIO.inspect(:ets.lookup(table, :ada))\nIO.inspect(:ets.lookup(table, :ada) |> length())\n:ets.insert(table, {:ada, \"v2\"})\nIO.inspect(:ets.lookup(table, :ada))\n:ets.delete(table, :ada)\nIO.inspect(:ets.lookup(table, :ada))",
              "note": "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."
            },
            {
              "kind": "table",
              "headers": [
                "Need",
                "Reach for"
              ],
              "rows": [
                [
                  "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"
                ]
              ]
            }
          ]
        }
      ]
    },
    {
      "id": "types",
      "title": "Types & the Type System",
      "icon": "shield-check",
      "blurb": "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.",
      "entries": [
        {
          "id": "types-gradual-typing",
          "title": "Gradual typing — zero annotations",
          "summary": "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.",
          "keywords": [
            "gradual typing",
            "type inference",
            "verified bugs",
            "dead code",
            "narrowing",
            "v1.20"
          ],
          "since": "v1.20",
          "exercises": null,
          "blocks": [
            {
              "kind": "code",
              "title": "The compiler type checks code you never annotated",
              "code": "defmodule Math do\n  def square(n), do: n * n          # inferred: number() -> number()\n\n  def run(word), do: square(String.length(word))\n\n  def broken do\n    square(:oops)   # type violation: atom() can never be number()\n  end\nend"
            },
            {
              "kind": "code",
              "title": "Redundant clauses and dead code are warnings now",
              "code": "def handle(msg) do\n  case msg do\n    {:ok, value} -> value\n    {:error, reason} -> reason\n    _other -> :unknown   # warning: this clause can never match\n  end\nend"
            },
            {
              "kind": "list",
              "items": [
                "**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"
              ]
            }
          ]
        },
        {
          "id": "types-dynamic",
          "title": "The dynamic() type",
          "summary": "`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.",
          "keywords": [
            "dynamic",
            "any",
            "compatibility",
            "narrowing",
            "gradual set",
            "disjoint"
          ],
          "since": "v1.20",
          "exercises": null,
          "blocks": [
            {
              "kind": "list",
              "items": [
                "**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"
              ]
            },
            {
              "kind": "code",
              "title": "Usage narrows the type",
              "code": "def total(data) do\n  # data starts as dynamic(); adding fields a and b\n  # narrows data to %{..., a: number(), b: number()}\n  data.a + data.b\nend"
            },
            {
              "kind": "code",
              "title": "Annotating gradual types in specs",
              "code": "@spec size_of(dynamic(integer() or binary())) :: integer()\ndef size_of(value) when is_integer(value), do: value\ndef size_of(value) when is_binary(value), do: byte_size(value)\n\n# passing an atom() to size_of/1 is a violation:\n# atom() is disjoint from integer() or binary()"
            }
          ]
        },
        {
          "id": "types-guards-inference",
          "title": "Guards, clauses & occurrence typing",
          "summary": "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).",
          "keywords": [
            "guards",
            "is_map_key",
            "tuple_size",
            "occurrence typing",
            "not_set",
            "inference",
            "clauses"
          ],
          "since": "v1.20",
          "exercises": null,
          "blocks": [
            {
              "kind": "code",
              "title": "Guards feed the type system",
              "code": "def first(list, default)\n      when is_list(list) and is_integer(default) do\n  # inferred here: list :: list(), default :: integer()\n  case list do\n    [] -> default\n    [head | _] -> head\n  end\nend"
            },
            {
              "kind": "code",
              "title": "is_map_key proves (and disproves) key presence",
              "code": "def name(user) when is_map_key(user, :name) do\n  user.name          # OK: user narrowed to %{..., name: dynamic()}\nend\n\ndef without_retries(config) when not is_map_key(config, :retries) do\n  config.retries     # violation: :retries is not_set() in this clause\nend\n\ndef small?(tuple) when tuple_size(tuple) < 3 do\n  elem(tuple, 3)     # violation: size is 0..2, index 3 is impossible\nend"
            },
            {
              "kind": "code",
              "title": "Occurrence typing across case/cond/with clauses",
              "code": "def upcase(value) do\n  case value do\n    nil -> \"nothing\"   # nil is removed from value in later clauses\n    other -> String.upcase(other)  # value cannot be nil here\n  end\nend",
              "note": "Union, intersection and negation of types flow through guards and clauses — including `case`, `cond`, `with` and multi-clause functions."
            }
          ]
        },
        {
          "id": "types-map-domain-keys",
          "title": "Map domain keys",
          "summary": "Non-atom map keys get real types: `%{123 => \"hello\"}` is `%{integer() => binary()}` — and `Map.put/delete/replace` results are tracked key by key.",
          "keywords": [
            "map",
            "domain keys",
            "integer keys",
            "map.put",
            "map.delete",
            "fetch",
            "not_set",
            "if_set"
          ],
          "since": "v1.20",
          "exercises": null,
          "blocks": [
            {
              "kind": "code",
              "title": "Domain keys are typed, and can mix with atom keys",
              "code": "sizes = %{123 => \"hello\"}\n# inferred: %{integer() => binary()}\n\nmixed = %{root: 1, 2 => 2}\n# inferred: %{integer() => integer(), root: integer()}"
            },
            {
              "kind": "code",
              "title": "Map.put/delete/change are key-tracked",
              "code": "def enable(map), do: Map.put(map, :debug, true)  # :debug is tracked\n\ndef drop(map) do\n  map = Map.delete(map, :debug)\n  map.debug      # violation: :debug is not_set() after Map.delete/2\nend\n\ndef swap(map), do: Map.replace(map, :debug, false)\n# Map.replace/3 keeps the key only if it was already set — if_set()"
            },
            {
              "kind": "code",
              "title": "Map.fetch!/2 is checked like direct access",
              "code": "def name!(map) do\n  Map.fetch!(map, :name)   # type-checked like map.name\nend\n\nname!(%{})            # warning: :name can never be present in %{}\nname!(%{name: \"Ada\"}) #=> \"Ada\""
            }
          ]
        },
        {
          "id": "types-typespecs-dialyzer",
          "title": "Typespecs & Dialyzer",
          "summary": "`@spec`/`@type` document contracts for humans and tools; the built-in checker (v1.20) needs none of them — Dialyzer remains the static-analysis veteran.",
          "keywords": [
            "typespec",
            "spec",
            "type",
            "typep",
            "opaque",
            "callback",
            "dialyzer",
            "success typing"
          ],
          "since": null,
          "exercises": null,
          "blocks": [
            {
              "kind": "code",
              "title": "Typespecs at a glance",
              "code": "defmodule Lexer do\n  @type token :: {:word, String.t()} | {:int, integer()}\n  @typep state :: %{tokens: [token()]}     # private to this module\n  @opaque ast :: {term(), [token()]}       # internals are hidden\n\n  @callback lex(String.t()) :: {:ok, ast()} | {:error, term()}\n\n  @spec tokenize(String.t()) :: [token()]\n  def tokenize(input) do\n    input\n    |> String.split(\" \")\n    |> Enum.map(&{:word, &1})\n  end\nend",
              "note": "`mix dialyzer` (via the `dialyxir` package) runs Erlang success typing over your specs and code."
            },
            {
              "kind": "table",
              "headers": [
                "Aspect",
                "Dialyzer (dialyxir)",
                "Built-in checker (v1.20)"
              ],
              "rows": [
                [
                  "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"
                ]
              ]
            }
          ]
        },
        {
          "id": "types-compiler-options",
          "title": "Compiler options for types & speed",
          "summary": "v1.20 adds `module_definition: :interpreted` for faster compiles of huge modules, and replaces `xref: [exclude:]` with `no_warn_undefined`.",
          "keywords": [
            "elixirc_options",
            "module_definition",
            "interpreted",
            "compiled",
            "no_warn_undefined",
            "xref",
            "compiler"
          ],
          "since": "v1.20",
          "exercises": null,
          "blocks": [
            {
              "kind": "code",
              "title": "mix.exs — project compiler options",
              "code": "def project do\n  [\n    app: :big_app,\n    elixirc_options: [\n      module_definition: :interpreted,   # faster builds for huge modules\n      no_warn_undefined: Legacy.Adapter  # replaces xref: [exclude: ...]\n    ]\n  ]\nend"
            },
            {
              "kind": "list",
              "items": [
                "**`: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"
              ]
            }
          ]
        }
      ]
    },
    {
      "id": "mix-testing",
      "title": "Mix & Testing",
      "icon": "flask-conical",
      "blurb": "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.",
      "entries": [
        {
          "id": "mix-testing-mix-essentials",
          "title": "Mix essentials & task discovery",
          "summary": "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).",
          "keywords": [
            "mix",
            "mix new",
            "deps",
            "app.tree",
            "mix help",
            "mix source",
            "task discovery",
            "partition"
          ],
          "since": null,
          "exercises": null,
          "blocks": [
            {
              "kind": "code",
              "title": "Everyday Mix",
              "lang": "sh",
              "code": "mix new my_app --sup             # OTP app + supervision tree\nmix new my_lib --umbrella        # umbrella project\nmix deps                         # list dependencies and their status\nmix deps.get                     # fetch dependencies\nmix deps.update decimal          # update one dep (mix deps.update --all: all)\nmix app.tree                     # print the runtime application tree\nmix app.tree --output tree.txt   # since v1.20: write the tree to a file\nMIX_OS_DEPS_COMPILE_PARTITION_COUNT=4 mix deps.compile   # v1.19+: parallel deps"
            },
            {
              "kind": "code",
              "title": "Finding tasks and docs",
              "lang": "sh",
              "code": "mix help                        # every available task\nmix help mix format             # docs for a specific task\nmix help String.trim/1          # module or fun/arity docs (since v1.19)\nmix help app:package            # tasks listed per application (since v1.19)\nmix source Ecto.Query           # print/open a module source (since v1.20)",
              "note": "Since v1.20, `mix help` output also renders `@type` and `@callback` documentation for the module you ask about."
            }
          ]
        },
        {
          "id": "mix-testing-exunit-basics",
          "title": "ExUnit basics",
          "summary": "`use ExUnit.Case`, `test` + `assert`/`refute`, `describe` blocks, tags and setup — with `--dry-run` (v1.20) and run counts on flaky retries (v1.20).",
          "keywords": [
            "exunit",
            "test",
            "assert",
            "refute",
            "describe",
            "tag",
            "setup",
            "dry-run",
            "repeat-until-failure"
          ],
          "since": null,
          "exercises": "075",
          "blocks": [
            {
              "kind": "code",
              "title": "A test module",
              "code": "ExUnit.start(seed: 1, max_cases: 1, autorun: false)\n\ndefmodule Calc do\n  def add(a, b), do: a + b\n\n  defmodule Cache do\n    use Agent\n\n    def start_link(_opts), do: Agent.start_link(fn -> %{} end, name: __MODULE__)\n  end\nend\n\ndefmodule CalcTest do\n  use ExUnit.Case, async: true\n\n  describe \"add/2\" do\n    @tag :smoke\n    test \"adds numbers\" do\n      assert Calc.add(1, 2) == 3\n      refute Calc.add(1, 2) == 4\n    end\n\n    test \"knows its floats\", _context do\n      assert Calc.add(0.1, 0.2) != 0.3   # classic float surprise\n    end\n  end\n\n  setup do\n    [cache: start_supervised!(Calc.Cache)]  # per-test, auto-stopped\n  end\nend\n\nExUnit.run()"
            },
            {
              "kind": "code",
              "title": "Running tests",
              "lang": "sh",
              "code": "mix test                             # everything\nmix test test/calc_test.exs          # one file\nmix test test/calc_test.exs:12       # the test that contains line 12\nmix test --only smoke                # tagged tests only\nmix test --exclude smoke             # everything except a tag\nmix test --dry-run                   # since v1.20: list tests, run none\nmix test --repeat-until-failure 100  # since v1.20: prints remaining runs while retrying",
              "note": "`--repeat-until-failure N` reruns the suite until it fails (flaky hunting); v1.20 shows how many runs remain as it goes."
            }
          ]
        },
        {
          "id": "mix-testing-assertions",
          "title": "Assertions & doctests",
          "summary": "`assert match?/2` for patterns, `assert_in_delta` for floats, `assert_raise` for errors — plus doctests that turn `@doc` examples into tests.",
          "keywords": [
            "assert",
            "match?",
            "assert_in_delta",
            "assert_raise",
            "doctest",
            "diff",
            "refute"
          ],
          "since": null,
          "exercises": "075",
          "blocks": [
            {
              "kind": "code",
              "title": "The assertion toolkit",
              "code": "ExUnit.start(seed: 1, max_cases: 1, autorun: false)\n\ndefmodule ToolkitTest do\n  use ExUnit.Case, async: true\n\n  test \"the assertion toolkit\" do\n    user = %{name: \"Ada\"}\n\n    assert match?(%{name: \"Ada\"}, user)      # does a pattern match?\n    assert_in_delta 0.3, 1 / 3, 0.001        # float-safe comparison\n    assert_raise ArithmeticError, fn -> div(1, 0) end\n    assert_raise RuntimeError, ~r/boom/, fn -> raise \"boom\" end\n\n    refute Enum.empty?([1])                  # assert not\n  end\nend\n\nExUnit.run()",
              "note": "Since v1.18 ExUnit diffs are much smarter — structural, colored comparisons for maps, lists and strings instead of inspect dumps."
            },
            {
              "kind": "code",
              "title": "Doctests — examples that must stay true",
              "code": "defmodule Greeting do\n  @doc \"\"\"\n  Builds a greeting.\n\n      iex> Greeting.build(\"Ada\")\n      \"Hello, Ada!\"\n  \"\"\"\n  def build(name), do: \"Hello, #{name}!\"\nend\n\ndefmodule GreetingTest do\n  use ExUnit.Case, async: true\n  doctest Greeting      # each iex> example becomes a test\nend\n\nExUnit.start(seed: 1, max_cases: 1, autorun: false)\nExUnit.run()"
            }
          ]
        },
        {
          "id": "mix-testing-good-tests",
          "title": "Properties of good tests",
          "summary": "Opinionated defaults: run everything **async**, supervise every process you start, wire heavy flows with aliases, and test behaviour — not internals.",
          "keywords": [
            "async",
            "start_supervised",
            "aliases",
            "fixtures",
            "flaky",
            "conventions"
          ],
          "since": null,
          "exercises": null,
          "blocks": [
            {
              "kind": "list",
              "items": [
                "**`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"
              ]
            }
          ]
        },
        {
          "id": "mix-testing-formatting-docs",
          "title": "Formatting & docs",
          "summary": "`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).",
          "keywords": [
            "mix format",
            "formatter",
            "import_deps",
            "migrate",
            "no-compile",
            "moduledoc",
            "doc",
            "documentation"
          ],
          "since": null,
          "exercises": null,
          "blocks": [
            {
              "kind": "code",
              "title": "Formatting commands",
              "lang": "sh",
              "code": "mix format                    # format the whole project\nmix format lib/my_app.ex      # one file\nmix format --check-formatted  # CI gate: exit 1 if anything is unformatted\nmix format --migrate          # since v1.15: also modernizes deprecated code\nmix format --no-compile       # since v1.20: format without compiling after"
            },
            {
              "kind": "code",
              "title": ".formatter.exs",
              "code": "[\n  import_deps: [:ecto, :phoenix],\n  line_length: 98,\n  inputs: [\"{mix,.formatter}.exs\", \"{config,lib,test}/**/*.{ex,exs}\"]\n]",
              "note": "`import_deps` pulls in each dependency's formatter config so macros like Ecto queries indent correctly."
            },
            {
              "kind": "code",
              "title": "Documentation conventions",
              "code": "defmodule Math do\n  @moduledoc \"\"\"\n  Helpers for integer math.\n  \"\"\"\n\n  @doc \"\"\"\n  Doubles a number.\n\n      iex> Math.twice(21)\n      42\n  \"\"\"\n  def twice(n), do: n * 2\n\n  @spec even?(integer()) :: boolean()\n  def even?(n), do: rem(n, 2) == 0\nend\n\nMath.twice(21)\n|> IO.inspect()\nMath.even?(42)\n|> IO.inspect()",
              "note": "`@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."
            }
          ]
        }
      ]
    },
    {
      "id": "protocols",
      "title": "Protocols & Behaviours",
      "icon": "network",
      "blurb": "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.",
      "entries": [
        {
          "id": "protocols-defprotocol",
          "title": "defprotocol / defimpl",
          "summary": "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.",
          "keywords": [
            "defprotocol",
            "defimpl",
            "string.chars",
            "inspect",
            "to_string",
            "derive",
            "polymorphism"
          ],
          "since": null,
          "exercises": "065–067",
          "blocks": [
            {
              "kind": "code",
              "title": "A protocol and two implementations",
              "code": "defprotocol Size do\n  @doc \"Elements, bytes — depends on the data\"\n  def size(data)\nend\n\ndefimpl Size, for: Map do\n  def size(map), do: map_size(map)\nend\n\ndefimpl Size, for: BitString do\n  def size(bin), do: byte_size(bin)\nend\n\nSize.size(%{a: 1, b: 2})   #=> 2\nSize.size(\"hello\")         #=> 5\n\nIO.inspect(Size.size(%{a: 1, b: 2}))\nIO.inspect(Size.size(\"hello\"))"
            },
            {
              "kind": "code",
              "title": "Built-ins: derive vs manual implementation",
              "code": "defmodule Account do\n  @enforce_keys [:id]\n  @derive {Inspect, only: [:id]}   # derive: trim what Inspect shows\n  defstruct [:id, plan: :free]\nend\n\n# implement manually for interpolation and to_string/1:\ndefimpl String.Chars, for: Account do\n  def to_string(account), do: \"Account##{account.id} (#{account.plan})\"\nend\ndefmodule Show do\n  def run do\n    to_string(%Account{id: 7})   #=> Account#7 (free)\n\n    IO.inspect(to_string(%Account{id: 7}))\n    IO.inspect(%Account{id: 7, plan: :pro})   # @derive Inspect trimmed the fields\n  end\nend\n\nShow.run()",
              "note": "Other built-ins: `Enumerable` (Enum/Stream), `Collectable`, `List.Chars`. Structs can also derive `Enumerable` or `Jason.Encoder`-style protocols from libraries."
            }
          ]
        },
        {
          "id": "protocols-dispatch-checking",
          "title": "Dispatch type checking",
          "summary": "Since v1.19 the compiler type checks protocol dispatch: interpolation warns when the type cannot implement `String.Chars`, `for` warns for non-Enumerable input.",
          "keywords": [
            "protocol",
            "dispatch",
            "string.chars",
            "enumerable",
            "interpolation",
            "comprehension",
            "warning"
          ],
          "since": "v1.19",
          "exercises": null,
          "blocks": [
            {
              "kind": "code",
              "title": "Caught before Protocol.UndefinedError",
              "code": "defmodule Demo do\n  def interpolate(range) do\n    \"#{range}\"   # warning: range() does not implement String.Chars\n  end\n\n  def iterate(number) do\n    for x <- number, do: x * 2   # warning: integer() is not Enumerable\n  end\nend",
              "note": "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."
            },
            {
              "kind": "list",
              "items": [
                "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"
              ]
            }
          ]
        },
        {
          "id": "protocols-fallback-any",
          "title": "@fallback_to_any — handle with care",
          "summary": "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.",
          "keywords": [
            "fallback_to_any",
            "any",
            "consolidation",
            "anti-pattern",
            "protocol undefined"
          ],
          "since": null,
          "exercises": "065–067",
          "blocks": [
            {
              "kind": "code",
              "title": "Fallback declaration",
              "code": "defprotocol Blank do\n  @fallback_to_any true\n  def blank?(data)\nend\n\ndefimpl Blank, for: Any do\n  def blank?(_data), do: false   # everything else is not blank\nend\n\ndefimpl Blank, for: List do\n  def blank?([]), do: true       # a specific impl wins over Any\n  def blank?(_), do: false\nend\n\nBlank.blank?([])    #=> true  (List impl)\nBlank.blank?(7)     #=> false (Any impl)\n\nIO.inspect([Blank.blank?([]), Blank.blank?(7), Blank.blank?(%{})])"
            },
            {
              "kind": "warn",
              "title": "Any fallback is an anti-pattern (usually)",
              "content": "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."
            }
          ]
        },
        {
          "id": "protocols-behaviours",
          "title": "Behaviours — contracts between modules",
          "summary": "`@callback` defines a module contract, `@behaviour` opts in, `@impl true` documents intent — and the compiler checks them all. `use` wires behaviours in via `__using__`.",
          "keywords": [
            "behaviour",
            "callback",
            "optional_callbacks",
            "impl",
            "use",
            "__using__",
            "genserver"
          ],
          "since": null,
          "exercises": "068–070",
          "blocks": [
            {
              "kind": "code",
              "title": "Define and implement a behaviour",
              "code": "defmodule MyApp.Pipeline.Stage do\n  @type stage :: term()\n\n  @callback init(opts :: term()) :: stage()\n  @callback run(event :: term(), stage()) :: {:ok, term()} | {:error, term()}\n\n  @optional_callbacks init: 1   # not implementing init/1 is fine\nend\n\ndefmodule MyApp.Pipeline.Log do\n  @behaviour MyApp.Pipeline.Stage\n\n  @impl true\n  def run(event, state) do\n    IO.puts(\"got #{inspect(event)}\")\n    {:ok, state}\n  end\nend\n\nMyApp.Pipeline.Log.run(:click, :state)   #=> got :click",
              "note": "Missing callbacks, wrong arities and stale implementations become compile warnings. `@impl true` also guards against defining a function nobody asked for."
            },
            {
              "kind": "code",
              "title": "use + __using__ injects behaviour wiring",
              "code": "defmodule MyApp.Pipeline.Stage do\n  defmacro __using__(opts) do\n    quote do\n      @behaviour MyApp.Pipeline.Stage\n      @default_opts unquote(opts)\n\n      @impl true\n      def init(opts), do: Keyword.merge(@default_opts, opts)\n      defoverridable init: 1\n    end\n  end\nend\n\ndefmodule Log do\n  use MyApp.Pipeline.Stage, level: :debug   # behaviour + init injected\nend\n\nLog.init([other: 1])\n|> IO.inspect(label: \"init merged defaults\")",
              "note": "`GenServer`, `Supervisor` and `Task` are behaviours — `use GenServer` injects the behaviour declaration, a `child_spec/1` and default callbacks you then override."
            }
          ]
        }
      ]
    },
    {
      "id": "metaprogramming",
      "title": "Metaprogramming",
      "icon": "sparkles",
      "blurb": "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.",
      "entries": [
        {
          "id": "metaprogramming-ast-quote",
          "title": "AST, quote & unquote",
          "summary": "`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.",
          "keywords": [
            "ast",
            "quote",
            "unquote",
            "macro.to_string",
            "string_to_quoted",
            "quoted expression",
            "tuple"
          ],
          "since": null,
          "exercises": null,
          "blocks": [
            {
              "kind": "code",
              "title": "Code as data (REPL)",
              "code": "iex> quote do: 1 + 2\n{:+, [context: Elixir, imports: [{1, Kernel}, {2, Kernel}]], [1, 2]}\n\niex> Code.string_to_quoted!(\"a + b\")\n{:+, [line: 1], [{:a, [line: 1], nil}, {:b, [line: 1], nil}]}\n\niex> Macro.to_string(quote do: Enum.map(list, &(&1 * 2)))\n\"Enum.map(list, &(&1 * 2))\"\n\nIO.inspect(quote(do: 1 + 2))\nIO.inspect(Code.string_to_quoted!(\"a + b\"))\nIO.inspect(Macro.to_string(quote(do: Enum.map(list, &(&1 * 2)))))"
            },
            {
              "kind": "list",
              "items": [
                "**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"
              ]
            }
          ]
        },
        {
          "id": "metaprogramming-defmacro",
          "title": "defmacro — compile-time code generation",
          "summary": "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.",
          "keywords": [
            "defmacro",
            "compile time",
            "expansion",
            "macro.escape",
            "ast",
            "unless",
            "timing"
          ],
          "since": null,
          "exercises": null,
          "blocks": [
            {
              "kind": "code",
              "title": "A timing macro",
              "code": "defmodule Timing do\n  defmacro timing(label, do: block) do\n    quote do\n      {elapsed, result} = :timer.tc(fn -> unquote(block) end)\n      _ = elapsed   # measured; printing it would make the output non-deterministic\n      IO.puts(\"#{unquote(label)}: #{inspect(result)}\")\n      result\n    end\n  end\nend\n\ndefmodule Runner do\n  import Timing\n\n  def run, do: timing(\"sum\", do: Enum.sum(1..1_000))\nend\n\nRunner.run()   #=> sum: 500500\n# the body above was inlined at compile time",
              "note": "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*."
            },
            {
              "kind": "warn",
              "title": "Macros receive AST, not values",
              "content": "`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."
            },
            {
              "kind": "code",
              "title": "Splicing: what a macro really receives",
              "code": "defmodule Oops do\n  # the argument arrives as AST, and it is spliced TWICE\n  defmacro double(x), do: quote(do: unquote(x) + unquote(x))\nend\n\ndefmodule Demo do\n  require Oops\n\n  def bump do\n    IO.puts(\"evaluated\")\n    1\n  end\n\n  def run, do: Oops.double(bump())\nend\n\nDemo.run()   #=> 2, and \"evaluated\" printed twice\n\n# precedence is safe: hygiene wraps spliced AST in parens\ndefmodule Safe do\n  require Oops\n  def run, do: Oops.double(2 + 3)\nend\n\nSafe.run()   #=> 10, not 8",
              "note": "Runtime **values** (maps, lists) must become AST before splicing: `Macro.escape(value)`. Atoms, numbers and binaries embed directly."
            }
          ]
        },
        {
          "id": "metaprogramming-use-using",
          "title": "use & __using__",
          "summary": "`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.",
          "keywords": [
            "use",
            "__using__",
            "inject",
            "genserver",
            "behaviour",
            "quote",
            "dsl"
          ],
          "since": null,
          "exercises": null,
          "blocks": [
            {
              "kind": "code",
              "title": "A tiny __using__ of your own",
              "code": "defmodule MiniLogger do\n  defmacro __using__(opts) do\n    level = Keyword.get(opts, :level, :info)   # runs at compile time\n\n    quote do\n      @level unquote(level)\n\n      def log(message), do: IO.puts(\"[#{@level}] \" <> message)\n    end\n  end\nend\n\ndefmodule MyApp do\n  use MiniLogger, level: :debug\n  def run, do: log(\"started\")    #=> [debug] started\nend\n\nMyApp.run()"
            },
            {
              "kind": "list",
              "items": [
                "**`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"
              ]
            }
          ]
        },
        {
          "id": "metaprogramming-hygiene",
          "title": "Hygiene & rules of thumb",
          "summary": "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.",
          "keywords": [
            "hygiene",
            "var!",
            "import",
            "macro",
            "function",
            "capture",
            "rules"
          ],
          "since": null,
          "exercises": null,
          "blocks": [
            {
              "kind": "code",
              "title": "A hygienic macro",
              "code": "defmodule Debug do\n  defmacro log_result(expr) do\n    quote do\n      result = unquote(expr)\n      IO.puts(\"got: #{inspect(result)}\")   # result is module-unique\n      result\n    end\n  end\nend\n\ndefmodule Demo do\n  import Debug\n  def go, do: log_result(2 + 3)   #=> got: 5 (and returns 5)\nend\n\nDemo.go()"
            },
            {
              "kind": "list",
              "items": [
                "**`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"
              ]
            },
            {
              "kind": "compare",
              "note": "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.",
              "left": {
                "title": "Macro (unneeded)",
                "code": "defmodule Bad do\n  defmacro greet(name) do\n    quote do: \"Hello, \" <> unquote(name)\n  end\nend\n\nBad.greet(\"Ada\")     # works...\n# &Bad.greet/1       # compile error: cannot capture a macro"
              },
              "right": {
                "title": "Function (just right)",
                "code": "defmodule Good do\n  def greet(name), do: \"Hello, \" <> name\nend\n\n\"Ada\" |> Good.greet()    # pipes\ncaps = &Good.greet/1     # captures\nGood.greet(\"Ada\")        # plain calls"
              }
            }
          ]
        }
      ]
    },
    {
      "id": "gotchas",
      "title": "Gotchas & Pitfalls",
      "icon": "bug",
      "blurb": "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.",
      "entries": [
        {
          "id": "gotchas-integer-division",
          "title": "/ always returns a float",
          "summary": "The `/` operator always returns a **float** — even for exact division. Use `div/2` and `rem/2` for integer math.",
          "keywords": [
            "division",
            "div",
            "rem",
            "float",
            "integer",
            "operator"
          ],
          "since": null,
          "exercises": null,
          "blocks": [
            {
              "kind": "compare",
              "note": "`Float.floor/1` or `trunc/1` can convert a float result, but `div/2` is the idiomatic integer division.",
              "left": {
                "title": "Trapped",
                "code": "10 / 4    #=> 2.5\n10 / 5    #=> 2.0   exact division is STILL a float\nis_float(10 / 5)   #=> true\n10 / 0    #=> ArithmeticError (never :infinity)"
              },
              "right": {
                "title": "Integer math",
                "code": "div(10, 4)   #=> 2     truncates toward zero\nrem(10, 4)   #=> 2     sign follows the dividend\ndiv(10, 0)   #=> ArithmeticError"
              }
            }
          ]
        },
        {
          "id": "gotchas-is-atom-nil",
          "title": "is_atom(nil) is true",
          "summary": "Booleans and `nil` **are atoms** — `true`, `false`, `nil` are just the atoms `:true`, `:false`, `:nil` with special literals.",
          "keywords": [
            "atom",
            "nil",
            "boolean",
            "guard",
            "is_atom",
            "surprise"
          ],
          "since": null,
          "exercises": null,
          "blocks": [
            {
              "kind": "warn",
              "title": "Guards over-match unless you narrow",
              "content": "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."
            },
            {
              "kind": "code",
              "code": "is_atom(nil)     #=> true   nil is the atom :nil\nis_atom(true)    #=> true   booleans are atoms\nis_boolean(nil)  #=> false  but not a boolean\n\n# only plain atoms like :ok:\n# the plain-atom test:\nis_atom(:ok) and not is_boolean(:ok) and not is_nil(:ok)\n\nIO.inspect([\n  is_atom(nil),\n  is_atom(true),\n  is_boolean(nil),\n  is_atom(:ok) and not is_boolean(:ok) and not is_nil(:ok),\n])"
            }
          ]
        },
        {
          "id": "gotchas-charlist-vs-string",
          "title": "Charlist vs string — the single-quote trap",
          "summary": "Single quotes create **charlists** (lists of codepoints), not strings — `'hello' == \"hello\"` is `false`, and lists of integers print as charlists.",
          "keywords": [
            "charlist",
            "string",
            "single quotes",
            "sigil",
            "inspect"
          ],
          "since": null,
          "exercises": null,
          "blocks": [
            {
              "kind": "compare",
              "note": "Erlang APIs (like `:crypto` or `:filename`) expect charlists — that is why they exist. In Elixir code, prefer strings.",
              "left": {
                "title": "Charlist",
                "code": "'hi' == \"hi\"        #=> false\nis_list('hi')       #=> true\nis_binary('hi')     #=> false\nIO.inspect('hi')    #=> 'hi'  looks like a string, is not\n[104, 105]          #=> 'hi'  lists of integers print as charlists"
              },
              "right": {
                "title": "String (binary)",
                "code": "\"hi\" == \"hi\"       #=> true\nis_binary(\"hi\")    #=> true  UTF-8 encoded binary\nString.to_charlist(\"hi\")   #=> 'hi'\nto_string('hi')    #=> \"hi\"  convert between the two"
              }
            }
          ]
        },
        {
          "id": "gotchas-partial-map-match",
          "title": "Map pattern matching is partial",
          "summary": "`%{name: _}` matches **any** map containing a `:name` key — extra keys are ignored. Use structs or a `map_size/1` guard when you need strictness.",
          "keywords": [
            "pattern matching",
            "map",
            "partial match",
            "struct",
            "map_size",
            "strict"
          ],
          "since": null,
          "exercises": null,
          "blocks": [
            {
              "kind": "compare",
              "note": "Partial matching is a **feature** for updating and defaulting — but a trap when you intended to assert the exact shape.",
              "left": {
                "title": "Surprising",
                "code": "def name(%{name: name}), do: name\n\nname(%{name: \"Ada\", junk: 1, extra: 2})\n#=> \"Ada\"  — matched anyway, extra keys ignored\n\nname(%{other: 1})\n#=> FunctionClauseError — key truly missing"
              },
              "right": {
                "title": "Strict",
                "code": "def name(%User{name: name}), do: name\n# structs match exact keys only\n\ndef only_name(map) when map_size(map) == 1,\n  do: map.name\n# guard rejects maps with more keys"
              }
            }
          ]
        },
        {
          "id": "gotchas-with-else",
          "title": "with without else swallows mismatches",
          "summary": "When no clause matches, a `with` without `else` returns the raw non-matching value **silently** — always provide an `else` for errors you care about.",
          "keywords": [
            "with",
            "else",
            "silent",
            "fallback",
            "pipeline",
            "error"
          ],
          "since": null,
          "exercises": null,
          "blocks": [
            {
              "kind": "compare",
              "note": "The returned non-matching value flows onward through your pipeline as if it were valid data — the bug surfaces far from the cause.",
              "left": {
                "title": "Silent fallthrough",
                "code": "with {:ok, user} <- fetch(id) do\n  user.name\nend\n# fetch/1 returned {:error, :not_found}?\n# the with returns {:error, :not_found} —\n# no raise, no warning, do block skipped"
              },
              "right": {
                "title": "Explicit else",
                "code": "with {:ok, user} <- fetch(id) do\n  user.name\nelse\n  {:error, reason} ->\n    Logger.warning(\"fetch failed: #{inspect(reason)}\")\n    {:error, reason}\nend"
              }
            }
          ]
        },
        {
          "id": "gotchas-struct-regex-default",
          "title": "Regex cannot be a struct field default",
          "summary": "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`.",
          "keywords": [
            "struct",
            "defstruct",
            "regex",
            "default",
            "new",
            "v1.19"
          ],
          "since": "v1.19",
          "exercises": null,
          "blocks": [
            {
              "kind": "compare",
              "note": "The same applies to any value that cannot live as a plain literal in compiled struct form.",
              "left": {
                "title": "Fails to compile (v1.19+)",
                "code": "defmodule Route do\n  defstruct pattern: ~r/foo/i\n  # error: invalid default for struct field :pattern\n  # regexes are not allowed as struct defaults (v1.19+)\nend"
              },
              "right": {
                "title": "Build in new/1",
                "code": "defmodule Route do\n  defstruct pattern: nil\n\n  def new(pattern \\\\ ~r/foo/i) do\n    %__MODULE__{pattern: pattern}\n  end\nend"
              }
            }
          ]
        },
        {
          "id": "gotchas-bitstring-size-pin",
          "title": "size(n) in patterns needs a pin",
          "summary": "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).",
          "keywords": [
            "bitstring",
            "binary",
            "size",
            "pin",
            "deprecation",
            "pattern",
            "v1.20"
          ],
          "since": "v1.20",
          "exercises": null,
          "blocks": [
            {
              "kind": "compare",
              "note": "The pin makes the hidden dependency (later segment sized by an earlier one) explicit — and lets the compiler check it.",
              "left": {
                "title": "Old form (hard-deprecated in v1.20)",
                "code": "def header(<<len, body::size(len)>>) do\n  # v1.20 warning: size must use the pin operator:\n  # expected <<len, body::size(^len)>>\nend"
              },
              "right": {
                "title": "Pinned (v1.20+)",
                "code": "def header(<<len, body::size(^len)>>) do\n  {:ok, body}   # len was bound by the earlier segment\nend"
              }
            }
          ]
        },
        {
          "id": "gotchas-file-stream-args",
          "title": "File.stream!/3 argument order swapped",
          "summary": "Hard-deprecated in v1.20: the old `(path, modes, lines_or_bytes)` order became `(path, lines_or_bytes, modes)`.",
          "keywords": [
            "file",
            "stream",
            "deprecation",
            "argument order",
            "v1.20"
          ],
          "since": "v1.20",
          "exercises": null,
          "blocks": [
            {
              "kind": "compare",
              "note": "`File.stream!/1` and `File.stream!/2` with a single option list keep working — only the 3-argument form changed order.",
              "left": {
                "title": "Old order (v1.19 and earlier)",
                "code": "File.stream!(\"app.log\", [:read, :utf8], 2048)\n# path, modes, lines_or_bytes   <- old order\n\n# v1.20 emits a deprecation warning (or errors)"
              },
              "right": {
                "title": "New order (v1.20+)",
                "code": "File.stream!(\"app.log\", 2048, [:read, :utf8])\n# path, lines_or_bytes, modes   <- new order\n\nFile.stream!(\"app.log\")        # defaults still fine"
              }
            }
          ]
        },
        {
          "id": "gotchas-atom-gc",
          "title": "Atoms are never garbage-collected",
          "summary": "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.",
          "keywords": [
            "atom",
            "garbage collection",
            "to_atom",
            "to_existing_atom",
            "dos",
            "memory"
          ],
          "since": null,
          "exercises": null,
          "blocks": [
            {
              "kind": "compare",
              "note": "The BEAM caps total atoms (~1M by default) — but the memory is allocated long before the limit hits.",
              "left": {
                "title": "Unbounded atoms",
                "code": "def metric(name) do\n  # every unique request mints a PERMANENT atom:\n  String.to_atom(\"metric_\" <> name)\nend\n# attacker sends millions of unique names -> OOM"
              },
              "right": {
                "title": "Existing atoms only",
                "code": "def metric(name) do\n  String.to_existing_atom(\"metric_\" <> name)\nend\n# raises ArgumentError for unknown atoms —\n# rescue it, or pre-register the valid set"
              }
            }
          ]
        },
        {
          "id": "gotchas-enum-vs-stream",
          "title": "Enum chains blow up memory — Stream does not",
          "summary": "Every chained `Enum` step builds a full intermediate list; `Stream` composes lazily — but a stream does nothing until an `Enum` function pulls it.",
          "keywords": [
            "enum",
            "stream",
            "lazy",
            "memory",
            "to_list",
            "pipeline",
            "large data"
          ],
          "since": null,
          "exercises": null,
          "blocks": [
            {
              "kind": "warn",
              "title": "Both directions bite",
              "content": "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."
            },
            {
              "kind": "code",
              "code": "# eager: 10M-element list, then another 5M-element list\n1..10_000_000\n|> Enum.map(&(&1 * 3))\n|> Enum.filter(&rem(&1, 2) == 0)\n|> Enum.sum()\n\n# lazy: one pass, constant memory\n1..10_000_000\n|> Stream.map(&(&1 * 3))\n|> Stream.filter(&rem(&1, 2) == 0)\n|> Enum.sum()      # <- the Enum call runs the stream\n\n1..1_000_000\n|> Enum.map(&(&1 * 3))\n|> Enum.filter(&rem(&1, 2) == 0)\n|> Enum.sum()\n|> IO.inspect(label: \"eager\")\n\n1..1_000_000\n|> Stream.map(&(&1 * 3))\n|> Stream.filter(&rem(&1, 2) == 0)\n|> Enum.sum()\n|> IO.inspect(label: \"lazy\")"
            }
          ]
        }
      ]
    },
    {
      "id": "reference-tables",
      "title": "Reference Tables",
      "icon": "table-2",
      "blurb": "Pin these to the wall: operator precedence, literals, sigils, guards, the standard library go-to functions, and everything new in Elixir v1.20.",
      "entries": [
        {
          "id": "reference-tables-operators",
          "title": "Operators & precedence",
          "summary": "Highest precedence first. `and`/`or` require a boolean left argument; `&&`/`||` accept any value where only `nil` and `false` are falsy.",
          "keywords": [
            "operators",
            "precedence",
            "association",
            "pipe",
            "comparison",
            "boolean"
          ],
          "since": null,
          "exercises": null,
          "blocks": [
            {
              "kind": "table",
              "headers": [
                "Precedence",
                "Operators",
                "Associativity",
                "Notes"
              ],
              "rows": [
                [
                  "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"
                ]
              ]
            },
            {
              "kind": "list",
              "items": [
                "**`==` 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"
              ]
            }
          ]
        },
        {
          "id": "reference-tables-literals",
          "title": "Literals quick reference",
          "summary": "Numbers with bases and underscores, atoms, strings vs charlists, bitstrings, collections — the syntax surface in one table.",
          "keywords": [
            "literals",
            "integer",
            "float",
            "atom",
            "binary",
            "tuple",
            "list",
            "map",
            "struct"
          ],
          "since": null,
          "exercises": null,
          "blocks": [
            {
              "kind": "table",
              "headers": [
                "Literal",
                "Syntax",
                "Notes"
              ],
              "rows": [
                [
                  "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"
                ]
              ]
            }
          ]
        },
        {
          "id": "reference-tables-sigils",
          "title": "Sigils",
          "summary": "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.",
          "keywords": [
            "sigil",
            "s",
            "c",
            "w",
            "regex",
            "date",
            "datetime",
            "heredoc"
          ],
          "since": null,
          "exercises": "071–073",
          "blocks": [
            {
              "kind": "table",
              "headers": [
                "Sigil",
                "Meaning",
                "Example"
              ],
              "rows": [
                [
                  "`~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"
                ]
              ]
            },
            {
              "kind": "list",
              "items": [
                "**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"
              ]
            }
          ]
        },
        {
          "id": "reference-tables-guards",
          "title": "Common guards",
          "summary": "Built-in type checks usable in `when` clauses — all safe, side-effect-free, and (since v1.20) feeding the type checker.",
          "keywords": [
            "guards",
            "is_integer",
            "is_binary",
            "is_map",
            "is_struct",
            "is_map_key",
            "when"
          ],
          "since": null,
          "exercises": null,
          "blocks": [
            {
              "kind": "table",
              "headers": [
                "Guard",
                "True when"
              ],
              "rows": [
                [
                  "`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`"
                ]
              ]
            },
            {
              "kind": "list",
              "items": [
                "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"
              ]
            }
          ]
        },
        {
          "id": "reference-tables-stdlib",
          "title": "Essential stdlib",
          "summary": "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.",
          "keywords": [
            "stdlib",
            "kernel",
            "enum",
            "string",
            "map",
            "list",
            "json",
            "process",
            "task"
          ],
          "since": null,
          "exercises": null,
          "blocks": [
            {
              "kind": "table",
              "headers": [
                "Module",
                "Go-to functions"
              ],
              "rows": [
                [
                  "`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"
                ]
              ]
            }
          ]
        },
        {
          "id": "reference-tables-new-in-1-20",
          "title": "New in v1.20 — quick table",
          "summary": "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.",
          "keywords": [
            "new",
            "v1.20",
            "changelog",
            "ceil_div",
            "popcount",
            "dry-run",
            "mix source"
          ],
          "since": "v1.20",
          "exercises": null,
          "blocks": [
            {
              "kind": "table",
              "headers": [
                "Addition",
                "Where",
                "Since"
              ],
              "rows": [
                [
                  "`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`"
                ]
              ]
            },
            {
              "kind": "list",
              "items": [
                "**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:]`"
              ]
            }
          ]
        }
      ]
    }
  ]
}
