From elixir
Provides Elixir best practices and OOP-to-functional shifts: avoid unnecessary processes, use pattern matching/with for control flow, {:ok/:error} handling, behaviors/protocols for polymorphism.
npx claudepluginhub georgeguimaraes/claude-code-elixir --plugin elixirThis skill uses the workspace's default tool permissions.
Mental shifts required before writing Elixir. These contradict conventional OOP patterns.
Writes idiomatic Elixir code with OTP patterns, supervision trees, Phoenix LiveView, and Ecto. Handles concurrency, fault tolerance, distributed systems, and BEAM optimizations. Use for refactoring or OTP design.
Routes to appropriate Elixir thinking skills before code exploration in projects with .ex files, mix.exs, or Elixir/Phoenix/Ecto/OTP mentions. Triggers on implement, add, fix, refactor tasks.
Writes idiomatic Elixir code with OTP patterns, supervision trees, Phoenix LiveView. Handles concurrency, fault tolerance, distributed systems.
Share bugs, ideas, or general feedback.
Mental shifts required before writing Elixir. These contradict conventional OOP patterns.
NO PROCESS WITHOUT A RUNTIME REASON
Before creating a GenServer, Agent, or any process, answer YES to at least one:
All three are NO? Use plain functions. Modules organize code; processes manage runtime.
OOP couples behavior, state, and mutability together. Elixir decouples them:
| OOP Dimension | Elixir Equivalent |
|---|---|
| Behavior | Modules (functions) |
| State | Data (structs, maps) |
| Mutability | Processes (GenServer) |
Pick only what you need. "I only need data and functions" = no process needed.
The misconception: Write careless code. The truth: Supervisors START processes.
{:ok, _} / {:error, _})Pattern matching first:
if/else or case in bodies%{} matches ANY map—use map_size(map) == 0 guard for empty mapscase—refactor to single case, with, or separate functionsError handling:
{:ok, result} / {:error, reason} for operations that can failwith for chaining {:ok, _} / {:error, _} operationsBe explicit about expected cases:
_ -> nil catch-alls—they silently swallow unexpected casesvalue && value.field nil-punning—obscures actual return types{:ok, nil} -> nil alongside {:ok, value} -> value.field, use with instead:# Verbose
case get_run(id) do
{:ok, nil} -> nil
{:ok, run} -> run.recommendations
end
# Prefer
with {:ok, %{recommendations: recs}} <- get_run(id), do: recs
| For Polymorphism Over... | Use | Contract |
|---|---|---|
| Modules | Behaviors | Upfront callbacks |
| Data | Protocols | Upfront implementations |
| Processes | Message passing | Implicit (send/receive) |
Behaviors = default for module polymorphism (very cheap at runtime) Protocols = only when composing data types, especially built-ins Message passing = only when stateful by design (IO, file handles)
Use the simplest abstraction: pattern matching → anonymous functions → behaviors → protocols → message passing. Each step adds complexity.
When justified: Library extensibility, multiple implementations, test swapping. When to stay coupled: Internal module, single implementation, pattern matching handles all cases.
OOP: Complex class hierarchy + visitor pattern. Elixir: Model as data + pattern matching + recursion.
{:sequence, {:literal, "rain"}, {:repeat, {:alternation, "dogs", "cats"}}}
def interpret({:literal, text}, input), do: ...
def interpret({:sequence, left, right}, input), do: ...
def interpret({:repeat, pattern}, input), do: ...
Use /3 variants (Keyword.get/3, Map.get/3) instead of case statements branching on nil:
# WRONG
case Keyword.get(opts, :chunker) do
nil -> chunker()
config -> parse_chunker_config(config)
end
# RIGHT
Keyword.get(opts, :chunker, :default) |> parse_chunker_config()
Don't create helper functions to merge config defaults. Inline the fallback:
# WRONG
defp merge_defaults(opts), do: Keyword.merge([repo: Application.get_env(:app, :repo)], opts)
# RIGHT
def some_function(opts) do
repo = opts[:repo] || Application.get_env(:app, :repo)
end
is_thing names for guards onlydefstruct [:name, :age][new | list] not list ++ [new]dbg/1 for debugging—prints formatted value with contextJSON module (Elixir 1.18+) instead of JasonAlways prefix mix commands with unbuffer to get ANSI colors and prevent stdout block-buffering in non-TTY environments (e.g. unbuffer mix test). Install: brew install expect (macOS) or apt install expect (Linux).
Prefer pattern matching over imperative assertions. Never use assert length + Enum.at/List.last/hd. Pattern match checks length and content in one shot:
# Bad
assert length(students) == 2
assert Enum.at(students, 0).name == "Alice"
assert Enum.at(students, 1).name == "Bob"
# Good
assert [%{name: "Alice"}, %{name: "Bob"}] = students
Test behavior, not implementation. Test use cases / public API. Refactoring shouldn't break tests.
Test your code, not the framework. If deleting your code doesn't fail the test, it's tautological.
Keep tests async. async: false means you've coupled to global state. Fix the coupling:
| Problem | Solution |
|---|---|
Application.put_env | Pass config as function argument |
| Feature flags | Inject via process dictionary or context |
| ETS tables | Create per-test tables with unique names |
| External APIs | Use Mox with explicit allowances |
| File system operations | Use @tag :tmp_dir (see below) |
Use tmp_dir for file tests. ExUnit creates unique temp directories per test, async-safe:
@tag :tmp_dir
test "writes file", %{tmp_dir: tmp_dir} do
path = Path.join(tmp_dir, "test.txt")
File.write!(path, "content")
assert File.read!(path) == "content"
end
Directory is auto-cleaned before each run. Works with @moduletag :tmp_dir for all tests in module.
| Excuse | Reality |
|---|---|
| "I need a process to organize this code" | Modules organize code. Processes are for runtime. |
| "GenServer is the Elixir way" | Plain functions are also the Elixir way. |
| "I'll need state eventually" | YAGNI. Add process when you need it. |
| "It's just a simple wrapper process" | Simple wrappers become bottlenecks. |
| "This is how I'd structure it in OOP" | Rethink from data flow. |
Any of these? Re-read The Iron Law.