skills/elixir/elixir-part1-core.md

209 lines
5.8 KiB
Markdown

# Elixir Part 1: Core Language & Modern Idioms
## Elixir 1.19 — What's New
### Gradual Set-Theoretic Type System
Elixir 1.19 significantly advances the built-in type system. It is **sound**, **gradual**, and **set-theoretic** (types compose via union, intersection, negation).
**Current capabilities (1.19):**
- Type inference from existing code — no annotations required yet
- **Protocol dispatch type checking** — warns on invalid protocol usage (e.g., string interpolation on structs without `String.Chars`)
- **Anonymous function type inference** — `fn` literals and `&captures` propagate types
- Types: `atom()`, `binary()`, `integer()`, `float()`, `pid()`, `port()`, `reference()`, `tuple()`, `list()`, `map()`, `function()`
- Compose: `atom() or integer()` (union), `atom() and not nil` (difference)
- `dynamic()` type for gradual typing — runtime-checked values
- Tuple precision: `{:ok, binary()}`, open tuples: `{:ok, binary(), ...}`
- Map types: `%{key: value}` closed, `%{optional(atom()) => term()}` open
- Function types: `(integer() -> boolean())`
### Up to 4x Faster Compilation
1. **Lazy module loading** — modules no longer loaded when defined; parallel compiler controls loading
2. **Parallel dependency compilation** — set `MIX_OS_DEPS_COMPILE_PARTITION_COUNT` env var
**Caveat:** If spawning processes during compilation that invoke sibling modules, use `Code.ensure_compiled!/1` first.
### Other 1.19 Features
- `min/2` and `max/2` allowed in guards
- `Access.values/0` — traverse all values in a map/keyword
- `String.count/2` — count occurrences
- Unicode 17.0.0, multi-line IEx prompts
- `mix help Mod.fun/arity`
- Erlang/OTP 28 support
---
## Breaking Changes Since 1.15
### Struct Update Syntax (1.18+)
```elixir
# The compiler now verifies the variable matches the struct type
%URI{my_uri | path: "/new"} # my_uri must be verified as %URI{}
```
### Regex as Struct Field Defaults (OTP 28)
```elixir
# BROKEN on OTP 28 — regex can't be struct field defaults
defstruct pattern: ~r/foo/ # Compile error
# FIX: use nil default, set at runtime
```
### Logger Backends Deprecated
```elixir
# OLD: config :logger, backends: [:console]
# NEW: use LoggerHandler (Erlang's logger)
config :logger, :default_handler, []
```
### Mix Task Separator
```elixir
# OLD: mix do compile, test
# NEW: mix do compile + test
```
### mix cli/0 Replaces Config Keys
```elixir
# OLD: default_task, preferred_cli_env in project/0
# NEW: def cli, do: [default_task: "phx.server", preferred_envs: [test: :test]]
```
---
## Core Language Patterns
### The Pipeline
```elixir
orders
|> Enum.filter(&(&1.status == :pending))
|> Enum.sort_by(& &1.created_at, DateTime)
|> Enum.map(&process_order/1)
```
### Pattern Matching
```elixir
# Function head matching — preferred over conditionals
def process(%Order{status: :pending} = order), do: ship(order)
def process(%Order{status: :shipped} = order), do: track(order)
def process(%Order{status: :delivered}), do: :noop
# Pin operator
expected = "hello"
^expected = some_function()
# Partial map matching
%{name: name} = %{name: "Michael", age: 42}
```
### With Expressions
```elixir
with {:ok, user} <- fetch_user(id),
{:ok, account} <- fetch_account(user),
{:ok, balance} <- check_balance(account) do
{:ok, balance}
else
{:error, :not_found} -> {:error, "User not found"}
error -> {:error, "Unknown: #{inspect(error)}"}
end
```
### Structs and Protocols
```elixir
defmodule Money do
defstruct [:amount, :currency]
defimpl String.Chars do
def to_string(%Money{amount: a, currency: c}), do: "#{a} #{c}"
end
end
```
### Behaviours
```elixir
defmodule PaymentProvider do
@callback charge(integer(), String.t()) :: {:ok, String.t()} | {:error, term()}
@callback refund(String.t()) :: {:ok, term()} | {:error, term()}
end
defmodule Stripe do
@behaviour PaymentProvider
@impl true
def charge(amount, currency), do: # ...
@impl true
def refund(transaction_id), do: # ...
end
```
---
## Mix Project Structure
```
my_app/
├── config/
│ ├── config.exs # Compile-time config
│ ├── dev.exs / prod.exs / test.exs
│ └── runtime.exs # Runtime config (secrets, env vars)
├── lib/
│ ├── my_app/
│ │ ├── application.ex # OTP Application + supervisor tree
│ │ └── ... # Domain modules
│ └── my_app_web/ # Phoenix web layer (if applicable)
├── priv/repo/migrations/
├── test/
├── mix.exs
├── .formatter.exs
└── AGENTS.md # Generated by usage_rules
```
---
## Testing
```elixir
defmodule MyApp.BlogTest do
use MyApp.DataCase, async: true
describe "creating posts" do
test "creates with valid attributes" do
assert {:ok, post} = Blog.create_post(%{title: "Hi", body: "World"})
assert post.title == "Hi"
end
test "fails without title" do
assert {:error, changeset} = Blog.create_post(%{body: "World"})
assert "can't be blank" in errors_on(changeset).title
end
end
end
```
Use `async: true` wherever possible. Use `Mox` for behaviour-based mocking.
---
## usage_rules — AI Agent Documentation
**Always include in every project.**
```elixir
# mix.exs deps
{:usage_rules, "~> 1.1", only: [:dev]}
# mix.exs project config
usage_rules: [packages: :all, output: :agents_md, mode: :linked]
```
```bash
mix usage_rules.gen # Generate AGENTS.md from deps
mix usage_rules.search_docs # Search hex documentation
mix usage_rules.gen_skill # Generate SKILL.md for Cowork
```
---
## Pro Tip: Local Docs
Elixir packages ship excellent local documentation. Once Elixir is installed on cortex, accessing docs locally via `mix hex.docs fetch <package>` or `h Module.function` in IEx may be more efficient than fetching URLs. Consider installing Elixir on cortex to enable this workflow.