elixir-development
npx skills add https://github.com/vircung/opencode-config --skill elixir-development
Agent 安装分布
Skill 文档
Elixir Development Skill
Modern Elixir Idioms (Version-Adaptive)
Pattern Matching and Guards
Basic Pattern Matching:
# Function heads with pattern matching
defmodule MyApp.User do
def greet(%User{name: name, status: :active}) do
"Hello, #{name}!"
end
def greet(%User{status: :inactive}) do
"Account suspended"
end
def greet(_), do: "Invalid user"
end
# Destructuring in function parameters
def process_response({:ok, %{"data" => data, "status" => status}}) do
%{data: data, status: String.to_atom(status)}
end
def process_response({:error, reason}) do
{:error, reason}
end
Guards for Additional Constraints:
defmodule MyApp.Math do
def divide(a, b) when is_number(a) and is_number(b) and b != 0 do
a / b
end
def divide(_, 0), do: {:error, :division_by_zero}
def divide(_, _), do: {:error, :invalid_arguments}
# Custom guard
defguard is_even(num) when is_integer(num) and rem(num, 2) == 0
def double_if_even(num) when is_even(num) do
num * 2
end
def double_if_even(_), do: {:error, :not_even}
end
Pin Operator for Matching Against Variables:
def update_if_changed(current_value, new_value) do
case {current_value, new_value} do
{^current_value, ^current_value} -> {:no_change, current_value}
{_, new} -> {:updated, new}
end
end
# Pattern matching in with clauses
def process_user_data(data) do
expected_version = "1.0"
with %{"version" => ^expected_version, "user" => user_data} <- data,
{:ok, user} <- create_user(user_data) do
{:ok, user}
else
%{"version" => version} -> {:error, {:invalid_version, version}}
error -> error
end
end
Pipe Operator and Function Composition
Effective Pipe Usage:
# Good: Clear data transformation pipeline
def process_orders(orders) do
orders
|> Enum.filter(&(&1.status == :pending))
|> Enum.map(&calculate_total/1)
|> Enum.sort_by(&(&1.total), :desc)
|> Enum.take(10)
end
# Avoid: Too many operations in one pipe
def bad_pipe_example(data) do
data
|> String.trim()
|> String.downcase()
|> String.split(",")
|> Enum.map(&String.trim/1)
|> Enum.filter(&(&1 != ""))
|> Enum.map(&String.to_integer/1) # This might fail!
|> Enum.sum()
end
# Better: Break into logical steps with error handling
def good_pipe_example(data) do
with {:ok, cleaned_data} <- clean_input(data),
{:ok, numbers} <- parse_numbers(cleaned_data) do
{:ok, Enum.sum(numbers)}
end
end
defp clean_input(data) do
cleaned =
data
|> String.trim()
|> String.downcase()
|> String.split(",")
|> Enum.map(&String.trim/1)
|> Enum.filter(&(&1 != ""))
{:ok, cleaned}
end
Function Composition Patterns:
# Compose functions for reusability
defmodule MyApp.DataProcessor do
def transform_user_data(data) do
data
|> normalize_keys()
|> validate_required_fields()
|> sanitize_inputs()
end
defp normalize_keys(data) do
Enum.map(data, fn {k, v} -> {String.to_atom(k), v} end) |> Map.new()
end
defp validate_required_fields(%{email: _, name: _} = data), do: {:ok, data}
defp validate_required_fields(_), do: {:error, :missing_required_fields}
defp sanitize_inputs({:ok, data}) do
sanitized = Map.update!(data, :name, &String.trim/1)
{:ok, sanitized}
end
defp sanitize_inputs(error), do: error
end
With Statements for Error Handling
Basic With Patterns:
def create_user_account(user_params, profile_params) do
with {:ok, user} <- create_user(user_params),
{:ok, profile} <- create_profile(profile_params, user.id),
{:ok, _} <- send_welcome_email(user) do
{:ok, %{user: user, profile: profile}}
else
{:error, changeset} when is_struct(changeset, Ecto.Changeset) ->
{:error, :validation_failed, changeset}
{:error, reason} ->
{:error, reason}
end
end
Advanced With Patterns:
def process_payment(order, payment_details) do
with {:ok, validated_order} <- validate_order(order),
{:ok, payment_method} <- validate_payment_method(payment_details),
{:ok, charge} <- charge_payment(payment_method, validated_order.amount),
{:ok, updated_order} <- update_order_status(validated_order, :paid),
:ok <- send_confirmation(updated_order) do
{:ok, updated_order}
else
{:error, :invalid_order} = error ->
Logger.error("Invalid order: #{inspect(order)}")
error
{:error, :payment_failed, reason} = error ->
Logger.warn("Payment failed for order #{order.id}: #{reason}")
error
{:error, reason} = error ->
Logger.error("Unexpected error processing payment: #{reason}")
error
end
end
Comprehensions for Data Processing
List Comprehensions:
# Basic comprehension
numbers = for n <- 1..10, do: n * n
# With filtering
even_squares = for n <- 1..10, rem(n, 2) == 0, do: n * n
# Multiple generators
coordinates = for x <- 1..3, y <- 1..3, do: {x, y}
# With pattern matching
users = [%{name: "Alice", age: 30}, %{name: "Bob", age: 25}]
adult_names = for %{name: name, age: age} <- users, age >= 18, do: name
Map and Set Comprehensions:
# Map comprehension
user_ages = for %{name: name, age: age} <- users, into: %{}, do: {name, age}
# Set comprehension
unique_ages = for %{age: age} <- users, into: MapSet.new(), do: age
# Custom collectible
string_result = for n <- 1..5, into: "", do: to_string(n)
Protocols and Behaviours
Defining Protocols:
defprotocol MyApp.Serializable do
@doc "Serialize data to string format"
def serialize(data)
end
defimpl MyApp.Serializable, for: Map do
def serialize(map) do
Jason.encode!(map)
end
end
defimpl MyApp.Serializable, for: List do
def serialize(list) do
list |> Enum.map(&to_string/1) |> Enum.join(",")
end
end
# Usage
MyApp.Serializable.serialize(%{name: "Alice"}) # JSON
MyApp.Serializable.serialize([1, 2, 3]) # "1,2,3"
Defining Behaviours:
defmodule MyApp.Storage do
@doc "Store data with given key"
@callback put(key :: String.t(), value :: term()) :: :ok | {:error, term()}
@doc "Retrieve data by key"
@callback get(key :: String.t()) :: {:ok, term()} | {:error, :not_found}
end
defmodule MyApp.MemoryStorage do
@behaviour MyApp.Storage
use Agent
def start_link(_), do: Agent.start_link(fn -> %{} end, name: __MODULE__)
@impl MyApp.Storage
def put(key, value) do
Agent.update(__MODULE__, &Map.put(&1, key, value))
:ok
end
@impl MyApp.Storage
def get(key) do
case Agent.get(__MODULE__, &Map.get(&1, key)) do
nil -> {:error, :not_found}
value -> {:ok, value}
end
end
end
Testing with ExUnit
Test Organization
Test Structure:
defmodule MyApp.AccountsTest do
use ExUnit.Case, async: true
import MyApp.Factory # For test data creation
alias MyApp.Accounts
describe "create_user/1" do
test "creates user with valid attributes" do
attrs = %{email: "user@example.com", name: "Test User"}
assert {:ok, user} = Accounts.create_user(attrs)
assert user.email == "user@example.com"
assert user.name == "Test User"
end
test "returns error with invalid email" do
attrs = %{email: "invalid", name: "Test User"}
assert {:error, changeset} = Accounts.create_user(attrs)
assert changeset.errors[:email]
end
end
describe "authenticate_user/2" do
setup do
user = insert(:user, password: "password123")
%{user: user}
end
test "returns user with valid credentials", %{user: user} do
assert {:ok, authenticated_user} = Accounts.authenticate_user(user.email, "password123")
assert authenticated_user.id == user.id
end
test "returns error with invalid password", %{user: user} do
assert {:error, :invalid_credentials} = Accounts.authenticate_user(user.email, "wrong")
end
end
end
Mocking Strategies
Using Mox for Behaviour Mocking:
# Define mock in test/support/mocks.ex
Mox.defmock(MyApp.MockPaymentGateway, for: MyApp.PaymentGateway)
defmodule MyApp.PaymentServiceTest do
use ExUnit.Case
import Mox
# Verify mocks are called
setup :verify_on_exit!
test "processes payment successfully" do
MyApp.MockPaymentGateway
|> expect(:charge, fn _amount, _card -> {:ok, %{id: "charge_123"}} end)
assert {:ok, result} = MyApp.PaymentService.process_payment(100, %{number: "4242"})
assert result.charge_id == "charge_123"
end
test "handles payment failure" do
MyApp.MockPaymentGateway
|> expect(:charge, fn _amount, _card -> {:error, :declined} end)
assert {:error, :payment_declined} = MyApp.PaymentService.process_payment(100, %{number: "4000"})
end
end
Testing Async Behavior:
defmodule MyApp.AsyncWorkerTest do
use ExUnit.Case
test "processes work asynchronously" do
test_pid = self()
# Start worker that sends message when done
{:ok, worker} = MyApp.AsyncWorker.start_link(fn result ->
send(test_pid, {:work_done, result})
end)
MyApp.AsyncWorker.enqueue_work(worker, :some_work)
# Wait for async completion
assert_receive {:work_done, :some_work}, 1000
end
end
Testing GenServers:
defmodule MyApp.CounterTest do
use ExUnit.Case
setup do
{:ok, counter} = MyApp.Counter.start_link(0)
%{counter: counter}
end
test "increments counter", %{counter: counter} do
assert MyApp.Counter.get(counter) == 0
:ok = MyApp.Counter.increment(counter)
assert MyApp.Counter.get(counter) == 1
end
test "handles concurrent access", %{counter: counter} do
# Spawn multiple processes incrementing
tasks = for _ <- 1..100 do
Task.async(fn -> MyApp.Counter.increment(counter) end)
end
# Wait for all to complete
Enum.each(tasks, &Task.await/1)
# Verify final count
assert MyApp.Counter.get(counter) == 100
end
end
Setup and Context
Shared Setup:
defmodule MyApp.IntegrationTest do
use ExUnit.Case
# Run before each test
setup do
# Reset database
Ecto.Adapters.SQL.Sandbox.checkout(MyApp.Repo)
# Create test data
user = insert(:user)
# Return context
%{user: user}
end
# Conditional setup
setup %{admin: true} do
admin_user = insert(:user, role: :admin)
%{admin_user: admin_user}
end
@tag admin: true
test "admin can access admin panel", %{admin_user: admin} do
# Test admin functionality
end
end
Functional Patterns
Recursion and Tail Call Optimization
Recursive Functions:
defmodule MyApp.ListUtils do
# Non-tail recursive (builds result on return)
def reverse_simple([]), do: []
def reverse_simple([head | tail]) do
reverse_simple(tail) ++ [head]
end
# Tail recursive with accumulator (more efficient)
def reverse(list), do: reverse(list, [])
defp reverse([], acc), do: acc
defp reverse([head | tail], acc) do
reverse(tail, [head | acc])
end
# Tree traversal
def sum_tree(%{value: value, left: nil, right: nil}), do: value
def sum_tree(%{value: value, left: left, right: right}) do
value + sum_tree(left) + sum_tree(right)
end
end
Higher-Order Functions
Function Composition:
defmodule MyApp.Transformers do
def compose(f, g) do
fn x -> f.(g.(x)) end
end
# Usage
def create_user_processor do
validate = &validate_user/1
normalize = &normalize_user/1
save = &save_user/1
validate |> compose(normalize) |> compose(save)
end
# Currying pattern
def multiply_by(factor) do
fn number -> number * factor end
end
def filter_and_transform(list, predicate, transformer) do
list
|> Enum.filter(predicate)
|> Enum.map(transformer)
end
# Usage
double = multiply_by(2)
is_even = fn n -> rem(n, 2) == 0 end
result = filter_and_transform([1, 2, 3, 4], is_even, double) # [4, 8]
end
State Management Without OTP
Immutable Update Patterns:
defmodule MyApp.GameState do
defstruct [:players, :score, :status]
def new do
%__MODULE__{
players: [],
score: %{},
status: :waiting
}
end
def add_player(state, player) do
%{state |
players: [player | state.players],
score: Map.put(state.score, player.id, 0)
}
end
def update_score(state, player_id, points) do
update_in(state.score[player_id], &(&1 + points))
end
def start_game(state) when length(state.players) >= 2 do
%{state | status: :playing}
end
def start_game(state), do: {:error, :not_enough_players}
end
Version-Specific Features
Elixir 1.14+ Features
Dbg Pipeline Debugging:
# Modern debugging with dbg
def process_data(data) do
data
|> dbg() # Shows value at this point
|> String.trim()
|> dbg() # Shows value after trim
|> String.upcase()
end
PartitionSupervisor Improvements:
# Better partition distribution in 1.14+
defmodule MyApp.WorkerSupervisor do
use PartitionSupervisor
def start_link(_opts) do
PartitionSupervisor.start_link(
MyApp.Worker,
strategy: :one_for_one,
partitions: System.schedulers_online(), # Optimal partitioning
name: __MODULE__
)
end
end
Elixir 1.15+ Features
ETS Improvements:
# Better ETS table types and operations
defmodule MyApp.FastCache do
def start_link do
:ets.new(:cache, [:named_table, :public, read_concurrency: true])
end
def put(key, value) do
:ets.insert(:cache, {key, value, :os.system_time(:second)})
end
# New select_replace in 1.15+
def expire_old_entries(max_age) do
cutoff = :os.system_time(:second) - max_age
:ets.select_delete(:cache, [{{:"$1", :"$2", :"$3"}, [{:<, :"$3", cutoff}], [true]}])
end
end
Legacy Compatibility (1.12-1.13)
Avoiding Newer Syntax:
# Use older syntax for compatibility
defmodule MyApp.LegacyCompatible do
# Instead of dbg(), use IO.inspect with labels
def debug_pipeline(data) do
data
|> IO.inspect(label: "Input")
|> String.trim()
|> IO.inspect(label: "Trimmed")
end
# Check version before using new features
if Version.match?(System.version(), ">= 1.14.0") do
def modern_feature do
# Use 1.14+ features
end
else
def modern_feature do
# Fallback implementation
end
end
end
Cross-References
Ecto Integration
For comprehensive Ecto patterns, schemas, changesets, queries, and database operations,
see the elixir-ecto skill.
OTP Patterns
For GenServer implementation, supervision trees, and process management patterns,
see the elixir-otp skill.
Phoenix Integration
For Phoenix framework patterns, generators, LiveView development, and web-specific testing,
see the elixir-phoenix-framework skill.
Best Practices Summary
Code Organization
- Use pattern matching liberally – it’s Elixir’s strength
- Prefer
withfor complex error handling – more readable than nested cases - Keep functions small and focused – easier to test and reason about
- Use descriptive function and variable names – code should read like prose
Performance Tips
- Tail call optimization – use accumulators for recursive functions
- Stream for large datasets – lazy evaluation saves memory
- Profile before optimizing – measure actual bottlenecks
Error Handling
- Fail fast with clear messages – don’t hide errors
- Use tagged tuples –
{:ok, result}and{:error, reason}pattern - Let it crash philosophy – proper supervision is better than defensive code
Testing Strategy
- Test behaviors, not implementation – focus on public APIs
- Use property-based testing – for complex logic verification
- Test async behavior carefully – use proper synchronization
This skill provides the foundation for writing idiomatic, maintainable Elixir code that leverages the language’s functional programming strengths and concurrent capabilities.