Claude-skill-registry lang-elixir-dev
Foundational Elixir patterns covering modules, pattern matching, processes, OTP behaviors (GenServer, Supervisor), Phoenix framework basics, and functional programming idioms. Use when writing Elixir code, building concurrent systems, working with Phoenix, or needing guidance on Elixir development patterns. This is the entry point for Elixir development.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/lang-elixir-dev" ~/.claude/skills/majiayu000-claude-skill-registry-lang-elixir-dev && rm -rf "$T"
skills/data/lang-elixir-dev/SKILL.mdElixir Fundamentals
Foundational Elixir patterns and core language features. This skill serves as both a reference for common patterns and an index to specialized Elixir skills.
Overview
┌─────────────────────────────────────────────────────────────────┐ │ Elixir Skill Hierarchy │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ┌───────────────────┐ │ │ │ lang-elixir-dev │ ◄── You are here │ │ │ (foundation) │ │ │ └─────────┬─────────┘ │ │ │ │ │ ┌────────────────────┼────────────────────┐ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ ┌─────────┐ ┌──────────┐ ┌──────────┐ │ │ │ phoenix │ │ otp │ │ ecto │ │ │ │ -dev │ │ -dev │ │ -dev │ │ │ └─────────┘ └──────────┘ └──────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘
This skill covers:
- Core syntax (modules, functions, pattern matching)
- Data types and immutability
- Pattern matching and guards
- Processes and message passing
- OTP basics (GenServer, Supervisor)
- Mix project structure
- Common functional programming idioms
- Phoenix framework fundamentals
This skill does NOT cover (see specialized skills):
- Advanced Phoenix features →
lang-elixir-phoenix-dev - Advanced OTP patterns →
lang-elixir-otp-dev - Database patterns with Ecto →
lang-elixir-ecto-dev - Deployment and releases →
lang-elixir-deploy-dev - Testing patterns →
lang-elixir-testing-dev
Quick Reference
| Task | Pattern |
|---|---|
| Define module | |
| Define function | |
| Private function | |
| Pattern match | |
| Pipe operator | |
| Spawn process | |
| Send message | |
| Receive message | |
| GenServer call | |
| Supervisor start | |
Skill Routing
Use this table to find the right specialized skill:
| When you need to... | Use this skill |
|---|---|
| Build web applications with Phoenix | |
| Design advanced OTP architectures | |
| Work with databases using Ecto | |
| Deploy applications, create releases | |
| Write tests with ExUnit | |
Core Data Types
Atoms
# Atoms are constants where name is the value :ok :error :atom_name :"atom with spaces" # Boolean atoms true # Same as :true false # Same as :false nil # Same as :nil # Module names are atoms IO # Same as :"Elixir.IO" String # Same as :"Elixir.String"
Numbers
# Integers (arbitrary precision) 42 1_000_000 0x1F # Hexadecimal 0o777 # Octal 0b1010 # Binary # Floats (64-bit double precision) 3.14 1.0e-10
Strings and Charlists
# Strings (UTF-8 binaries) "hello" "hello #{name}" # Interpolation "multi line string" # String operations String.upcase("hello") # "HELLO" String.length("hello") # 5 String.split("a,b,c", ",") # ["a", "b", "c"] String.replace("hello", "l", "x") # "hexxo" # Charlists (lists of integers) 'hello' # [104, 101, 108, 108, 111] 'hello' ++ ' world' # 'hello world'
Lists
# Lists (linked lists) [1, 2, 3] [head | tail] = [1, 2, 3] # head = 1, tail = [2, 3] # List operations [1, 2] ++ [3, 4] # [1, 2, 3, 4] (concatenation) [1, 2, 3] -- [2] # [1, 3] (difference) hd([1, 2, 3]) # 1 (head) tl([1, 2, 3]) # [2, 3] (tail) length([1, 2, 3]) # 3 # List module Enum.map([1, 2, 3], fn x -> x * 2 end) # [2, 4, 6] Enum.filter([1, 2, 3], fn x -> rem(x, 2) == 0 end) # [2] Enum.reduce([1, 2, 3], 0, fn x, acc -> x + acc end) # 6
Tuples
# Tuples (contiguous memory) {:ok, "value"} {:error, :not_found} {1, 2, 3} # Common pattern: tagged tuples def divide(a, b) when b != 0, do: {:ok, a / b} def divide(_, 0), do: {:error, :division_by_zero} # Tuple operations elem({:ok, 42}, 0) # :ok put_elem({:a, :b}, 1, :c) # {:a, :c} tuple_size({:a, :b, :c}) # 3
Maps
# Maps (key-value stores) %{:name => "Alice", :age => 30} %{name: "Alice", age: 30} # Shorthand for atom keys # Accessing values map = %{name: "Alice", age: 30} map[:name] # "Alice" map.name # "Alice" (only for atom keys) # Updating maps %{map | age: 31} # Update existing key Map.put(map, :city, "NYC") # Add new key Map.delete(map, :age) # Remove key # Pattern matching %{name: name} = %{name: "Alice", age: 30} # name = "Alice" # Map operations Map.keys(map) # [:name, :age] Map.values(map) # ["Alice", 30] Map.merge(map1, map2) # Merge maps
Keyword Lists
# Keyword lists (lists of 2-tuples with atom keys) [name: "Alice", age: 30] # Same as: [{:name, "Alice"}, {:age, 30}] # Common in function options String.split("a,b,c", ",", trim: true) # Accessing values list = [name: "Alice", age: 30] list[:name] # "Alice" Keyword.get(list, :name) # "Alice" # Can have duplicate keys [a: 1, a: 2, a: 3]
Pattern Matching
Basic Patterns
# Match operator {a, b, c} = {1, 2, 3} # a=1, b=2, c=3 # List matching [head | tail] = [1, 2, 3] # head=1, tail=[2,3] [first, second | rest] = [1, 2, 3, 4] # first=1, second=2, rest=[3,4] # Tuple matching {:ok, result} = {:ok, 42} # result=42 {:error, reason} = {:error, :not_found} # reason=:not_found # Map matching %{name: name} = %{name: "Alice", age: 30} # name="Alice" %{name: name, age: age} = %{name: "Alice", age: 30} # Pin operator (match against value) x = 1 ^x = 1 # OK ^x = 2 # MatchError
Function Pattern Matching
# Multiple function clauses def greet(:morning), do: "Good morning!" def greet(:afternoon), do: "Good afternoon!" def greet(:evening), do: "Good evening!" # Pattern match on structure def handle_response({:ok, data}), do: process(data) def handle_response({:error, reason}), do: handle_error(reason) # Pattern match on lists def sum([]), do: 0 def sum([head | tail]), do: head + sum(tail) # Ignore values with underscore def process({:ok, _}), do: :success def process({:error, reason}), do: {:failed, reason}
Guards
# Guard clauses def positive?(x) when x > 0, do: true def positive?(_), do: false # Multiple guards (AND) def adult?(age) when is_integer(age) and age >= 18, do: true def adult?(_), do: false # Multiple guards (OR) def number?(x) when is_integer(x) or is_float(x), do: true def number?(_), do: false # Common guard functions is_atom(x) is_binary(x) is_boolean(x) is_list(x) is_map(x) is_number(x) is_tuple(x) is_nil(x)
Modules and Functions
Module Definition
defmodule Math do # Module attribute (compile-time constant) @pi 3.14159 # Public function def circle_area(radius) do @pi * radius * radius end # Private function defp validate_radius(radius) when radius > 0, do: :ok defp validate_radius(_), do: :error # One-line function def double(x), do: x * 2 # Function with multiple clauses def factorial(0), do: 1 def factorial(n) when n > 0, do: n * factorial(n - 1) # Function with default arguments def greet(name, greeting \\ "Hello") do "#{greeting}, #{name}!" end end
Anonymous Functions
# Define anonymous function add = fn a, b -> a + b end add.(1, 2) # 3 (note the dot) # Shorthand syntax add = &(&1 + &2) add.(1, 2) # 3 # Capture operator with named functions doubled = Enum.map([1, 2, 3], &(&1 * 2)) lengths = Enum.map(["a", "ab", "abc"], &String.length/1) # Multiple clauses handle = fn {:ok, result} -> result {:error, _} -> nil end
Function Composition
# Pipe operator "hello" |> String.upcase() |> String.reverse() # "OLLEH" # Function capture numbers = [1, 2, 3, 4, 5] evens = Enum.filter(numbers, &(rem(&1, 2) == 0)) # Composition example def process_data(data) do data |> String.trim() |> String.downcase() |> String.split(",") |> Enum.map(&String.trim/1) |> Enum.reject(&(&1 == "")) end
Processes and Concurrency
Spawning Processes
# Spawn a process pid = spawn(fn -> receive do {:hello, sender} -> send(sender, {:ok, "Hello back!"}) end end) # Send message send(pid, {:hello, self()}) # Receive message receive do {:ok, message} -> IO.puts(message) after 1000 -> IO.puts("Timeout!") end
Process Links and Monitoring
# Spawn linked process (failures propagate) pid = spawn_link(fn -> raise "Error!" end) # Trap exits to handle linked process failures Process.flag(:trap_exit, true) receive do {:EXIT, pid, reason} -> IO.puts("Process died: #{inspect(reason)}") end # Monitor process (doesn't link) {:ok, pid} = some_process() ref = Process.monitor(pid) receive do {:DOWN, ^ref, :process, ^pid, reason} -> IO.puts("Process down: #{inspect(reason)}") end
Process State with Recursion
# Counter process defmodule Counter do def start(initial_value) do spawn(fn -> loop(initial_value) end) end defp loop(value) do receive do {:increment, caller} -> send(caller, {:ok, value + 1}) loop(value + 1) {:get, caller} -> send(caller, {:ok, value}) loop(value) :stop -> :ok end end end # Usage counter = Counter.start(0) send(counter, {:increment, self()}) receive do {:ok, new_value} -> IO.puts("New value: #{new_value}") end
OTP Basics
GenServer
defmodule Stack do use GenServer # Client API def start_link(initial_stack) do GenServer.start_link(__MODULE__, initial_stack, name: __MODULE__) end def push(item) do GenServer.cast(__MODULE__, {:push, item}) end def pop do GenServer.call(__MODULE__, :pop) end # Server Callbacks @impl true def init(initial_stack) do {:ok, initial_stack} end @impl true def handle_call(:pop, _from, [head | tail]) do {:reply, head, tail} end def handle_call(:pop, _from, []) do {:reply, nil, []} end @impl true def handle_cast({:push, item}, state) do {:noreply, [item | state]} end end # Usage {:ok, _pid} = Stack.start_link([]) Stack.push(1) Stack.push(2) Stack.pop() # 2
Supervisor
defmodule MyApp.Application do use Application @impl true def start(_type, _args) do children = [ # Worker {Stack, []}, # Worker with custom config {MyWorker, name: MyWorker, restart: :transient}, # Supervisor {Registry, keys: :unique, name: MyApp.Registry} ] opts = [strategy: :one_for_one, name: MyApp.Supervisor] Supervisor.start_link(children, opts) end end # Restart strategies: # :one_for_one - restart only failed child # :one_for_all - restart all children if one fails # :rest_for_one - restart failed child and those started after it
GenServer with State Management
defmodule Counter do use GenServer # Client def start_link(initial_value \\ 0) do GenServer.start_link(__MODULE__, initial_value, name: __MODULE__) end def increment do GenServer.call(__MODULE__, :increment) end def get do GenServer.call(__MODULE__, :get) end def async_increment do GenServer.cast(__MODULE__, :increment) end # Server @impl true def init(initial_value) do {:ok, initial_value} end @impl true def handle_call(:increment, _from, state) do {:reply, state + 1, state + 1} end def handle_call(:get, _from, state) do {:reply, state, state} end @impl true def handle_cast(:increment, state) do {:noreply, state + 1} end end
Mix Project Structure
Creating a New Project
# Create new project mix new my_app # Create new supervised application mix new my_app --sup # Project structure: # my_app/ # ├── lib/ # │ ├── my_app.ex # │ └── my_app/ # │ └── application.ex # ├── test/ # │ ├── my_app_test.exs # │ └── test_helper.exs # ├── mix.exs # └── README.md
mix.exs Configuration
defmodule MyApp.MixProject do use Mix.Project def project do [ app: :my_app, version: "0.1.0", elixir: "~> 1.14", start_permanent: Mix.env() == :prod, deps: deps() ] end def application do [ extra_applications: [:logger], mod: {MyApp.Application, []} ] end defp deps do [ {:phoenix, "~> 1.7"}, {:ecto_sql, "~> 3.10"}, {:postgrex, ">= 0.0.0"}, {:jason, "~> 1.4"}, {:plug_cowboy, "~> 2.6"} ] end end
Common Mix Tasks
# Compile project mix compile # Run tests mix test # Run application mix run --no-halt # Interactive shell iex -S mix # Get dependencies mix deps.get # Format code mix format # Create new module mix phx.gen.context Accounts User users name:string email:string
Phoenix Framework Basics
Phoenix Project Structure
my_app/ ├── assets/ # Frontend assets ├── config/ # Configuration ├── lib/ │ ├── my_app/ # Business logic │ ├── my_app_web/ # Web interface │ │ ├── controllers/ │ │ ├── views/ │ │ ├── templates/ │ │ └── router.ex │ └── my_app.ex ├── priv/ # Database migrations, static files └── test/
Router
defmodule MyAppWeb.Router do use MyAppWeb, :router pipeline :browser do plug :accepts, ["html"] plug :fetch_session plug :fetch_flash plug :protect_from_forgery plug :put_secure_browser_headers end pipeline :api do plug :accepts, ["json"] end scope "/", MyAppWeb do pipe_through :browser get "/", PageController, :index resources "/users", UserController end scope "/api", MyAppWeb do pipe_through :api resources "/posts", PostController, except: [:new, :edit] end end
Controller
defmodule MyAppWeb.UserController do use MyAppWeb, :controller alias MyApp.Accounts alias MyApp.Accounts.User def index(conn, _params) do users = Accounts.list_users() render(conn, "index.html", users: users) end def show(conn, %{"id" => id}) do user = Accounts.get_user!(id) render(conn, "show.html", user: user) end def create(conn, %{"user" => user_params}) do case Accounts.create_user(user_params) do {:ok, user} -> conn |> put_flash(:info, "User created successfully.") |> redirect(to: ~p"/users/#{user}") {:error, %Ecto.Changeset{} = changeset} -> render(conn, "new.html", changeset: changeset) end end end
LiveView Basics
defmodule MyAppWeb.CounterLive do use MyAppWeb, :live_view @impl true def mount(_params, _session, socket) do {:ok, assign(socket, count: 0)} end @impl true def handle_event("increment", _params, socket) do {:noreply, update(socket, :count, &(&1 + 1))} end def handle_event("decrement", _params, socket) do {:noreply, update(socket, :count, &(&1 - 1))} end @impl true def render(assigns) do ~H""" <div> <h1>Count: <%= @count %></h1> <button phx-click="increment">+</button> <button phx-click="decrement">-</button> </div> """ end end
Common Patterns and Idioms
With Statement
# Chain operations that can fail def create_user(params) do with {:ok, validated} <- validate_params(params), {:ok, user} <- insert_user(validated), {:ok, email} <- send_welcome_email(user) do {:ok, user} else {:error, reason} -> {:error, reason} end end
Case Statement
case File.read("config.json") do {:ok, contents} -> Jason.decode(contents) {:error, :enoent} -> {:error, "File not found"} {:error, reason} -> {:error, "Read error: #{inspect(reason)}"} end
Cond Statement
cond do age < 13 -> "child" age < 20 -> "teenager" age < 60 -> "adult" true -> "senior" end
Using Structs
defmodule User do defstruct [:name, :email, age: 0, active: true] def new(name, email) do %User{name: name, email: email} end def activate(%User{} = user) do %{user | active: true} end end # Usage user = %User{name: "Alice", email: "alice@example.com"} user = User.activate(user) # Pattern matching def greet(%User{name: name}), do: "Hello, #{name}!"
Protocols
# Define protocol defprotocol Serializable do def serialize(data) end # Implement for different types defimpl Serializable, for: Map do def serialize(map), do: Jason.encode!(map) end defimpl Serializable, for: List do def serialize(list), do: Jason.encode!(list) end # Usage Serializable.serialize(%{name: "Alice"}) Serializable.serialize([1, 2, 3])
Use Directive
# Define reusable behavior defmodule MyMacro do defmacro __using__(opts) do quote do import MyMacro @opts unquote(opts) def common_function do "This is common" end end end end # Use it defmodule MyModule do use MyMacro, option: :value end
Enum and Stream
Enum Module (Eager)
# Map Enum.map([1, 2, 3], fn x -> x * 2 end) # [2, 4, 6] # Filter Enum.filter([1, 2, 3, 4], fn x -> rem(x, 2) == 0 end) # [2, 4] # Reduce Enum.reduce([1, 2, 3], 0, fn x, acc -> x + acc end) # 6 # Find Enum.find([1, 2, 3], fn x -> x > 2 end) # 3 # Chaining [1, 2, 3, 4, 5] |> Enum.filter(&(rem(&1, 2) == 0)) |> Enum.map(&(&1 * 2)) # [4, 8] # Common functions Enum.any?([1, 2, 3], &(&1 > 2)) # true Enum.all?([1, 2, 3], &(&1 > 0)) # true Enum.count([1, 2, 3]) # 3 Enum.sum([1, 2, 3]) # 6 Enum.max([1, 2, 3]) # 3 Enum.min([1, 2, 3]) # 1 Enum.sort([3, 1, 2]) # [1, 2, 3] Enum.uniq([1, 2, 2, 3]) # [1, 2, 3] Enum.zip([1, 2], [:a, :b]) # [{1, :a}, {2, :b}]
Stream Module (Lazy)
# Lazy operations (only computed when needed) stream = Stream.map([1, 2, 3], fn x -> x * 2 end) Enum.to_list(stream) # [2, 4, 6] # Infinite streams Stream.iterate(0, &(&1 + 1)) |> Stream.take(5) |> Enum.to_list() # [0, 1, 2, 3, 4] # File streaming (memory efficient) File.stream!("large_file.txt") |> Stream.map(&String.trim/1) |> Stream.filter(&(&1 != "")) |> Enum.count()
Error Handling
Try-Rescue
try do raise "Error!" rescue e in RuntimeError -> "Caught runtime error: #{e.message}" e -> "Caught: #{inspect(e)}" after cleanup() end
Try-Catch
try do throw(:error) catch :error -> "Caught thrown value" :exit, _ -> "Caught exit" end
Result Tuples
# Preferred pattern in Elixir def divide(a, b) when b != 0, do: {:ok, a / b} def divide(_, 0), do: {:error, :division_by_zero} # Usage case divide(10, 2) do {:ok, result} -> "Result: #{result}" {:error, reason} -> "Error: #{reason}" end # With pattern matching {:ok, result} = divide(10, 2) # Raises if not :ok
Troubleshooting
Match Error
Problem:
MatchError: no match of right hand side value
# Cause: Pattern doesn't match {:ok, value} = {:error, :not_found} # MatchError! # Fix: Use case or handle both patterns case result do {:ok, value} -> value {:error, reason} -> handle_error(reason) end
Undefined Function
Problem:
UndefinedFunctionError
# Cause: Function not defined or not imported String.upcase("hello") # OK upcase("hello") # UndefinedFunctionError # Fix: Import or use full module name import String upcase("hello") # OK
Process Crashes
Problem: Process dies unexpectedly
# Use supervisors to restart failed processes children = [ {MyWorker, restart: :permanent} ] Supervisor.start_link(children, strategy: :one_for_one)
Genserver Timeout
Problem: GenServer call times out
# Default timeout is 5 seconds GenServer.call(server, :slow_operation) # May timeout # Increase timeout GenServer.call(server, :slow_operation, 30_000) # 30 seconds # Or use cast for async operations GenServer.cast(server, :slow_operation)
Testing
Elixir has excellent testing support built-in with ExUnit, along with doctests, property-based testing, and mocking capabilities.
ExUnit Basics
# test/math_test.exs defmodule MathTest do use ExUnit.Case # Test with assertion test "add/2 adds two numbers" do assert Math.add(2, 3) == 5 assert Math.add(-1, 1) == 0 end # Refute (opposite of assert) test "add/2 does not return incorrect sum" do refute Math.add(2, 3) == 6 end # Pattern matching in assertions test "divide/2 returns ok tuple" do assert {:ok, result} = Math.divide(10, 2) assert result == 5 end # Testing errors test "divide/2 returns error on division by zero" do assert {:error, :division_by_zero} = Math.divide(10, 0) end end
Test Lifecycle
defmodule UserTest do use ExUnit.Case # Setup runs before each test setup do user = %User{name: "Alice", age: 30} {:ok, user: user} end # Access setup data via context test "user has name", %{user: user} do assert user.name == "Alice" end # Setup with explicit context setup context do if context[:admin] do {:ok, user: %User{name: "Admin", role: :admin}} else :ok end end @tag :admin test "admin user has correct role", %{user: user} do assert user.role == :admin end # Setup all (runs once before all tests) setup_all do # Start database connection {:ok, conn} = Database.connect() on_exit(fn -> Database.disconnect(conn) end) {:ok, conn: conn} end end
Assertions
# Equality assert 1 + 1 == 2 refute 1 + 1 == 3 # Pattern matching assert {:ok, value} = function_that_returns_tuple() assert %User{name: name} = get_user() # Boolean assert is_binary("hello") assert is_list([1, 2, 3]) # Membership assert 3 in [1, 2, 3] refute 4 in [1, 2, 3] # Approximate equality (floats) assert_in_delta 0.1 + 0.2, 0.3, 0.0001 # Exception testing assert_raise ArithmeticException, fn -> 1 / 0 end assert_raise ArgumentError, "invalid argument", fn -> raise ArgumentError, "invalid argument" end # Receive message testing send(self(), {:hello, "world"}) assert_receive {:hello, msg} assert msg == "world" # No message received refute_receive {:unexpected, _}, 100
Doctests
defmodule Math do @doc """ Adds two numbers together. ## Examples iex> Math.add(2, 3) 5 iex> Math.add(-1, 1) 0 iex> Math.add(0.1, 0.2) 0.30000000000000004 """ def add(a, b), do: a + b @doc """ Divides two numbers. ## Examples iex> Math.divide(10, 2) {:ok, 5.0} iex> Math.divide(10, 0) {:error, :division_by_zero} """ def divide(_a, 0), do: {:error, :division_by_zero} def divide(a, b), do: {:ok, a / b} end # In test file, enable doctests defmodule MathTest do use ExUnit.Case doctest Math end
Async Tests
# Run tests asynchronously (safe if no shared state) defmodule FastTest do use ExUnit.Case, async: true test "independent test 1" do assert 1 + 1 == 2 end test "independent test 2" do assert 2 * 2 == 4 end end # Synchronous tests (default, for shared resources) defmodule DatabaseTest do use ExUnit.Case # async: false is default test "writes to database" do # Safe to share database end end
Test Tags and Filtering
defmodule UserTest do use ExUnit.Case # Tag individual test @tag :slow test "slow integration test" do # ... end # Tag with value @tag timeout: 5000 test "test with custom timeout" do # ... end # Multiple tags @tag :integration @tag :database test "database integration" do # ... end # Module-level tags (apply to all tests) @moduletag :integration end
# Run only tagged tests mix test --only slow mix test --only integration # Exclude tagged tests mix test --exclude slow mix test --exclude integration:database # Include by default excluded tests mix test --include pending
Mocking with Mox
# Define behaviour defmodule WeatherAPI do @callback get_temperature(city :: String.t()) :: {:ok, float()} | {:error, term()} end # Define mock in test_helper.exs Mox.defmock(WeatherAPIMock, for: WeatherAPI) # In your module, inject dependency defmodule WeatherService do def get_weather(city, api \\ WeatherAPI) do case api.get_temperature(city) do {:ok, temp} -> "Temperature in #{city}: #{temp}°C" {:error, _} -> "Could not fetch weather" end end end # In test defmodule WeatherServiceTest do use ExUnit.Case, async: true import Mox # Set up expectations setup :verify_on_exit! test "returns temperature message" do expect(WeatherAPIMock, :get_temperature, fn "NYC" -> {:ok, 25.0} end) result = WeatherService.get_weather("NYC", WeatherAPIMock) assert result == "Temperature in NYC: 25.0°C" end test "handles API errors" do expect(WeatherAPIMock, :get_temperature, fn _city -> {:error, :timeout} end) result = WeatherService.get_weather("NYC", WeatherAPIMock) assert result == "Could not fetch weather" end test "allows multiple calls" do stub(WeatherAPIMock, :get_temperature, fn _city -> {:ok, 20.0} end) WeatherService.get_weather("NYC", WeatherAPIMock) WeatherService.get_weather("SF", WeatherAPIMock) # Both calls succeed with stub end end
Property-Based Testing with StreamData
# Add to mix.exs defp deps do [ {:stream_data, "~> 0.6", only: :test} ] end # Property-based test defmodule StringPropertiesTest do use ExUnit.Case use ExUnitProperties property "reversing a string twice returns original" do check all string <- string(:printable) do reversed_twice = string |> String.reverse() |> String.reverse() assert reversed_twice == string end end property "list concatenation is associative" do check all list1 <- list_of(integer()), list2 <- list_of(integer()), list3 <- list_of(integer()) do assert (list1 ++ list2) ++ list3 == list1 ++ (list2 ++ list3) end end property "sorting is idempotent" do check all list <- list_of(integer()) do sorted_once = Enum.sort(list) sorted_twice = Enum.sort(sorted_once) assert sorted_once == sorted_twice end end # Custom generator property "user age is always positive" do check all name <- string(:alphanumeric), age <- positive_integer() do user = %User{name: name, age: age} assert User.valid?(user) end end end
Testing Processes and GenServers
defmodule CounterTest do use ExUnit.Case test "counter increments" do {:ok, pid} = Counter.start_link(0) Counter.increment(pid) Counter.increment(pid) assert Counter.get(pid) == 2 end test "counter handles cast" do {:ok, pid} = Counter.start_link(0) Counter.async_increment(pid) # Give it time to process cast Process.sleep(10) assert Counter.get(pid) == 1 end test "counter can be supervised" do children = [ {Counter, 0} ] {:ok, supervisor} = Supervisor.start_link(children, strategy: :one_for_one) # Get counter pid from supervisor [{Counter, pid, _, _}] = Supervisor.which_children(supervisor) Counter.increment(pid) assert Counter.get(pid) == 1 # Clean up Supervisor.stop(supervisor) end end
Testing with ExUnit.CaptureIO
import ExUnit.CaptureIO test "prints greeting to stdout" do output = capture_io(fn -> IO.puts("Hello, World!") end) assert output == "Hello, World!\n" end test "captures user input" do result = capture_io("Alice\n", fn -> name = IO.gets("Enter name: ") String.trim(name) end) assert result == "Alice" end test "captures stderr" do output = capture_io(:stderr, fn -> IO.warn("Warning message") end) assert output =~ "Warning message" end
Common Testing Patterns
# Test with multiple assertions using pipe test "user creation pipeline" do params = %{name: "Alice", email: "alice@example.com"} assert {:ok, user} = params |> User.changeset() |> Repo.insert() assert user.name == "Alice" assert user.email == "alice@example.com" end # Test with pattern matching and guards test "validates positive numbers" do assert {:ok, result} = Math.sqrt(4) assert result == 2.0 assert {:error, :negative_number} = Math.sqrt(-1) end # Test with describe blocks for organization describe "User.create/1" do test "creates user with valid params" do # ... end test "returns error with invalid email" do # ... end test "returns error with duplicate email" do # ... end end # Test with shared setup using tags setup context do case context[:user_type] do :admin -> {:ok, user: create_admin_user()} :regular -> {:ok, user: create_regular_user()} _ -> :ok end end @tag user_type: :admin test "admin can delete users", %{user: admin} do assert admin.role == :admin end
Metaprogramming
Elixir provides powerful metaprogramming capabilities through macros, which operate on the Abstract Syntax Tree (AST) at compile time.
Quote and Unquote
# quote turns code into AST representation quote do 1 + 2 end # {:+, [context: Elixir, imports: [{1, Kernel}, {2, Kernel}]], [1, 2]} # unquote injects values into quoted expressions defmodule Example do x = 10 quoted = quote do unquote(x) + 5 end # {+, _, [10, 5]} end # unquote_splicing for lists args = [1, 2, 3] quote do sum(unquote_splicing(args)) end # {:sum, [], [1, 2, 3]}
Defining Macros
defmodule MyMacros do # Basic macro defmacro say(expression) do quote do IO.puts(unquote(expression)) end end # Macro with variable hygiene defmacro double(x) do quote do result = unquote(x) result * 2 end end # Debugging macro - shows expression and result defmacro debug(expression) do quote bind_quoted: [expr: expression] do IO.inspect(expr, label: unquote(Macro.to_string(expression))) end end end # Usage require MyMacros MyMacros.say("Hello!") MyMacros.debug(1 + 2) # 1 + 2: 3
The using Macro
defmodule MyBehaviour do # __using__ is called when `use MyBehaviour` is invoked defmacro __using__(opts) do quote do import MyBehaviour @behaviour MyBehaviour # Inject default implementations def default_name, do: unquote(opts[:name] || "Unknown") # Allow override defoverridable default_name: 0 end end @callback required_callback() :: term() end # Usage defmodule MyModule do use MyBehaviour, name: "Custom" end
AST Manipulation
# Traverse and transform AST defmodule ASTHelper do def transform(ast) do Macro.prewalk(ast, fn {:+, meta, [left, right]} -> {:-, meta, [left, right]} # Replace + with - node -> node end) end # Expand macros in AST def expand_all(ast, env) do Macro.expand(ast, env) end # Convert AST to string def to_string(ast) do Macro.to_string(ast) end end # Inspect AST structure quote do: if(true, do: 1, else: 2) |> Macro.to_string() # "if(true, do: 1, else: 2)"
Compile-Time Code Generation
defmodule Router do # Generate functions at compile time from data @routes [ {:get, "/", :index}, {:get, "/users", :users}, {:post, "/users", :create_user} ] for {method, path, handler} <- @routes do def route(unquote(method), unquote(path)) do apply(__MODULE__, unquote(handler), []) end end def index, do: "Home page" def users, do: "List users" def create_user, do: "Create user" end # Also useful: Module.register_attribute/3 for accumulating data defmodule PluginHost do Module.register_attribute(__MODULE__, :plugins, accumulate: true) @plugins :auth @plugins :logging @plugins :caching def plugins, do: @plugins # [:caching, :logging, :auth] end
Serialization
Elixir uses Jason (or Poison) for JSON serialization and Protocol-based encoding for custom types.
Jason (Recommended)
# Add to mix.exs: {:jason, "~> 1.4"} # Encoding Jason.encode!(%{name: "Alice", age: 30}) # "{\"name\":\"Alice\",\"age\":30}" # Decoding Jason.decode!("{\"name\":\"Alice\",\"age\":30}") # %{"name" => "Alice", "age" => 30} # With atom keys Jason.decode!("{\"name\":\"Alice\"}", keys: :atoms) # %{name: "Alice"} # Pretty printing Jason.encode!(%{user: %{name: "Alice"}}, pretty: true)
Implementing Jason.Encoder Protocol
defmodule User do @derive {Jason.Encoder, only: [:id, :name, :email]} defstruct [:id, :name, :email, :password_hash] end # Custom encoder implementation defmodule Money do defstruct [:amount, :currency] end defimpl Jason.Encoder, for: Money do def encode(%Money{amount: amount, currency: currency}, opts) do Jason.Encode.string("#{currency} #{amount}", opts) end end # Usage Jason.encode!(%Money{amount: 100, currency: "USD"}) # "\"USD 100\""
Poison (Alternative)
# Add to mix.exs: {:poison, "~> 5.0"} Poison.encode!(%{name: "Alice"}) Poison.decode!("{\"name\":\"Alice\"}") # Implementing Poison.Encoder defimpl Poison.Encoder, for: DateTime do def encode(datetime, options) do Poison.Encoder.BitString.encode(DateTime.to_iso8601(datetime), options) end end
Ecto Changesets for Validation
defmodule User do use Ecto.Schema import Ecto.Changeset schema "users" do field :name, :string field :email, :string field :age, :integer timestamps() end def changeset(user, attrs) do user |> cast(attrs, [:name, :email, :age]) |> validate_required([:name, :email]) |> validate_format(:email, ~r/@/) |> validate_number(:age, greater_than: 0) end end # Validate incoming JSON params = Jason.decode!(json_string) changeset = User.changeset(%User{}, params) if changeset.valid? do {:ok, Ecto.Changeset.apply_changes(changeset)} else {:error, changeset.errors} end
Term Serialization
# Erlang Term Format (binary) binary = :erlang.term_to_binary(%{key: "value", list: [1, 2, 3]}) :erlang.binary_to_term(binary) # External Term Format (for distributed systems) binary = :erlang.term_to_binary(data, [:compressed]) # Safe deserialization (atoms must exist) :erlang.binary_to_term(binary, [:safe])
REPL and Development Workflow
IEx (Interactive Elixir) is central to Elixir development, providing a powerful REPL with debugging, introspection, and hot code reloading.
Starting IEx
# Basic IEx iex # With Mix project loaded iex -S mix # With Phoenix server iex -S mix phx.server # With custom configuration iex --dot-iex path/to/.iex.exs -S mix
IEx Helpers
# In IEx session: # Help and documentation h Enum.map/2 # Function docs h Enum # Module docs t Enum.t() # Type specs # Code inspection i [1, 2, 3] # Inspect value i Enum # Inspect module # Compilation c "path/to/file.ex" # Compile file r MyModule # Recompile module recompile() # Recompile project # Value history v() # Last result v(1) # First result v(-1) # Previous result # Shell commands pwd() # Current directory ls() # List files cd("path") # Change directory
IEx.pry for Debugging
defmodule MyModule do def process(data) do transformed = transform(data) # Insert breakpoint require IEx; IEx.pry() finalize(transformed) end end # When code hits pry: # - Inspect local variables: transformed, data # - Call functions: transform(other_data) # - Continue: respawn() or Ctrl+C twice
Hot Code Reloading
# Recompile and reload module in IEx recompile() # Reload specific module r MyModule # For Phoenix - automatic in dev mode # Code changes trigger recompilation on next request # In production (Distillery/Release) # Use hot code upgrades via :code.load_file/1 :code.purge(MyModule) :code.load_file(MyModule)
Observer for System Inspection
# Start Observer (GUI) :observer.start() # Observer shows: # - System overview (memory, CPU, processes) # - Process list with message queues # - Application supervision trees # - ETS tables # - Port info # For remote nodes Node.connect(:"app@hostname") :observer.start() # Then select remote node in Nodes menu
.iex.exs Configuration
# In ~/.iex.exs or project .iex.exs # Custom aliases alias MyApp.{Repo, User, Account} # Import helpers import Ecto.Query # Custom helpers defmodule H do def reload do IEx.Helpers.recompile() IO.puts("Reloaded!") end def user(id), do: Repo.get(User, id) end # Configure IEx IEx.configure( colors: [enabled: true], history_size: 100, inspect: [limit: :infinity] )
Runtime Debugging
# Trace function calls :dbg.tracer() :dbg.p(:all, :c) :dbg.tp(MyModule, :my_function, :x) # Now calls to MyModule.my_function will be traced # Stop tracing :dbg.stop() # Using :recon for production debugging # Add {:recon, "~> 2.5"} to mix.exs :recon.proc_count(:memory, 10) # Top 10 by memory :recon.proc_count(:message_queue_len, 10) # Top 10 by queue :recon.bin_leak(5) # Binary memory leaks
Zero and Default Values
Elixir handles absence of values through nil and pattern matching, with explicit default handling patterns.
Nil Handling
# nil is a valid value (not an error) user = nil is_nil(user) # true # Nil-safe access with pattern matching case get_user(id) do nil -> {:error, :not_found} user -> {:ok, user} end # Nil coalescing with || name = user_name || "Anonymous" # Access with default Map.get(map, :key, "default") Keyword.get(opts, :timeout, 5000) # Nil-safe navigation (no ?. operator, use pattern matching) defp get_city(user) do case user do %{address: %{city: city}} -> city _ -> nil end end # Or with get_in get_in(user, [:address, :city])
Default Function Arguments
defmodule Config do # Default arguments def connect(host, port \\ 80, opts \\ []) do timeout = Keyword.get(opts, :timeout, 5000) ssl = Keyword.get(opts, :ssl, false) {host, port, timeout, ssl} end end # Multiple clauses with defaults Config.connect("localhost") # port=80, opts=[] Config.connect("localhost", 443) # opts=[] Config.connect("localhost", 443, ssl: true)
Struct Defaults
defmodule User do # All fields with defaults defstruct name: "Unknown", email: nil, role: :user, active: true, metadata: %{} # Enforce required fields @enforce_keys [:email] defstruct [:email, name: "Unknown", role: :user] end # Creating with defaults %User{email: "test@example.com"} # %User{email: "test@example.com", name: "Unknown", role: :user} # Pattern match with defaults def greet(%User{name: name}) do "Hello, #{name}!" end
Default Values in Maps
# Access with default map = %{a: 1, b: 2} Map.get(map, :c, 0) # 0 # Update with default Map.update(map, :c, 0, &(&1 + 1)) # %{a: 1, b: 2, c: 0} # get_and_update with default Map.get_and_update(map, :c, fn nil -> {nil, 0} val -> {val, val + 1} end) # Merge with defaults defaults = %{timeout: 5000, retries: 3} config = %{timeout: 10000} Map.merge(defaults, config) # %{timeout: 10000, retries: 3}
With Construct for Nil Propagation
# Chain operations that may return nil def process_order(order_id) do with {:ok, order} <- fetch_order(order_id), {:ok, user} <- fetch_user(order.user_id), {:ok, payment} <- process_payment(user, order) do {:ok, %{order: order, user: user, payment: payment}} else nil -> {:error, :not_found} {:error, reason} -> {:error, reason} end end # Helper for nil-returning functions defp fetch_order(id) do case Repo.get(Order, id) do nil -> {:error, :order_not_found} order -> {:ok, order} end end
Default Behaviours
defmodule Cache do @callback get(key :: term()) :: {:ok, term()} | :error @callback put(key :: term(), value :: term()) :: :ok # Optional callback with default @callback ttl() :: integer() @optional_callbacks ttl: 0 defmacro __using__(_opts) do quote do @behaviour Cache # Default implementation def ttl, do: 3600 defoverridable ttl: 0 end end end defmodule MyCache do use Cache def get(key), do: # ... def put(key, value), do: # ... # ttl/0 uses default of 3600 end
Cross-Cutting Patterns
For cross-language comparison and translation patterns, see:
- Processes, GenServers, supervision treespatterns-concurrency-dev
- JSON encoding/decoding, protocolspatterns-serialization-dev
- Macros, compile-time code generationpatterns-metaprogramming-dev
- Testing strategies, property-based testingpatterns-testing-dev
References
- Elixir Language
- Elixir School
- Phoenix Framework
- Hex Package Manager
- Elixir Forum
- ExUnit Documentation
- Mox Documentation
- StreamData Documentation
- Specialized skills:
,lang-elixir-phoenix-dev
,lang-elixir-otp-devlang-elixir-ecto-dev