返回 Skill 列表
extension
分类: 开发与工程无需 API Key

lang-elixir-dev

涵盖模块、模式匹配、进程、OTP行为(GenServer、Supervisor)、Phoenix框架基础和函数式编程惯用法的基础Elixir模式。在编写Elixir代码、构建并发系统、使用Phoenix工作或需要关于Elixir开发模式的指导时使用。这是Elixir开发的入门点。

person作者: jakexiaohubgithub

Elixir 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 | defmodule MyModule do ... end | | Define function | def function_name(args), do: result | | Private function | defp private_function(args), do: result | | Pattern match | {:ok, value} = result | | Pipe operator | value \|> function1() \|> function2() | | Spawn process | spawn(fn -> ... end) | | Send message | send(pid, message) | | Receive message | receive do pattern -> ... end | | GenServer call | GenServer.call(pid, message) | | Supervisor start | Supervisor.start_link(children, opts) |


Skill Routing

Use this table to find the right specialized skill:

| When you need to... | Use this skill | |---------------------|----------------| | Build web applications with Phoenix | lang-elixir-phoenix-dev | | Design advanced OTP architectures | lang-elixir-otp-dev | | Work with databases using Ecto | lang-elixir-ecto-dev | | Deploy applications, create releases | lang-elixir-deploy-dev | | Write tests with ExUnit | lang-elixir-testing-dev |


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:

  • patterns-concurrency-dev - Processes, GenServers, supervision trees
  • patterns-serialization-dev - JSON encoding/decoding, protocols
  • patterns-metaprogramming-dev - Macros, compile-time code generation
  • patterns-testing-dev - Testing strategies, property-based testing

References