skills/elixir/elixir-part1-core.md

5.8 KiB

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 inferencefn 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+)

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

# 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

# OLD: config :logger, backends: [:console]
# NEW: use LoggerHandler (Erlang's logger)
config :logger, :default_handler, []

Mix Task Separator

# OLD: mix do compile, test
# NEW: mix do compile + test

mix cli/0 Replaces Config Keys

# 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

orders
|> Enum.filter(&(&1.status == :pending))
|> Enum.sort_by(& &1.created_at, DateTime)
|> Enum.map(&process_order/1)

Pattern Matching

# 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

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

defmodule Money do
  defstruct [:amount, :currency]

  defimpl String.Chars do
    def to_string(%Money{amount: a, currency: c}), do: "#{a} #{c}"
  end
end

Behaviours

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

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.

# mix.exs deps
{:usage_rules, "~> 1.1", only: [:dev]}

# mix.exs project config
usage_rules: [packages: :all, output: :agents_md, mode: :linked]
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.