Write idiomatic Elixir/Erlang with efficiency patterns
A before/after Elixir/Erlang reference for pattern matching, pipes, OTP processes, error handling, and Enum use.
Maintainer of this project? Claim this page to edit the listing.
13.5.0Add to Favorites
Why it matters
Developers hire this skill to write clean, performant Elixir and Erlang code by following language-specific idioms, avoiding anti-patterns, and leveraging BEAM VM features like pattern matching, OTP processes, and lazy evaluation.
Outcomes
What it gets done
Replace nested conditionals with pattern matching and guard clauses
Refactor eager Enum chains into lazy Stream pipelines for large datasets
Convert raw spawn calls into supervised GenServers and Tasks
Transform nested case statements into with expressions for error handling
Install
Add it to your toolbox
Run in your project directory:
curl -fsSL https://spark.entire.vc/get/ag-elixir | bash Overview
Elixir / Erlang: Idiomatic Efficiency Reference
A before/after Elixir/Erlang reference covering pattern matching, pipes, OTP processes, error handling, and collections. Use when writing or reviewing Elixir or Erlang code and want idiomatic BEAM-platform patterns applied consistently.
What it does
Elixir / Erlang: Idiomatic Efficiency Reference is a skill of before/after Elixir coding guidelines across seven areas: pattern matching and guards, the pipe operator, processes and OTP, error handling, collections and Enum, structs and protocols, and a summary anti-pattern table.
When to use - and when NOT to
Use this when writing or reviewing Elixir or Erlang code and want idiomatic BEAM-platform patterns applied consistently. It's language-specific guidance, not an architectural framework, and the skill notes over-compression can hurt readability, so judgement still applies.
Inputs and outputs
Pattern matching: destructure directly with case or pattern match instead of Map.get plus a nil check, use a with clause instead of nested case blocks for chained {:ok, _} and {:error, _} steps, and prefer multi-clause functions with pattern-matched arguments and guard clauses, such as when is_integer(x) and x > 0, over if/else branching on a value's shape or type. Pipes: chain two or more transforms with |> instead of nesting function calls, use then/1 or a named function rather than an awkward inline anonymous-function pipe, and skip the pipe entirely for a single operation. Processes and OTP: use GenServer instead of raw spawn for stateful processes, use Task.start or Task.async/await instead of unlinked spawn for fire-and-forget or awaitable work, use Registry or a named GenServer instead of manual Process.register, and never wrap a GenServer callback in try/catch - "let it crash" and let the supervisor restart it, since defensive catching inside a GenServer breaks supervision. Error handling: return tagged tuples like {:error, :not_found} for expected failures instead of raising, always handle both branches of an {:ok, _} or {:error, _} result rather than pattern-matching only the success case, prefer atom or struct error reasons over free-text strings since they're matchable and cheap, and use with instead of deeply nested ok/error case blocks.
Integrations
Collections: use a for comprehension when filtering and transforming together instead of chaining Enum.filter and Enum.map, use Enum.empty?/1 or a [] pattern match instead of Enum.count(x) == 0 which traverses the whole list, use Map.new/2 instead of Enum.reduce to build a map, and switch to Stream for lazy evaluation when chaining transforms over large or infinite collections, keeping Enum for small collections or the final step. Structs and protocols: use a defstruct with @enforce_keys instead of a plain map for domain entities, so a typo'd key fails instead of going unnoticed; skip defprotocol until a second implementation is actually needed; and use put_in or update_in instead of manually rebuilding nested structs. A closing anti-pattern table maps specific bad patterns - unlinked spawn, try/catch in a GenServer, string error reasons, length(list) > 0, raw PID passing, ETS without a GenServer or Agent wrapper - to their preferred replacements.
Who it's for
Elixir and Erlang developers writing or reviewing BEAM-platform code who want a concrete before/after reference for OTP process design, error handling, and idiomatic collection use instead of relying on habit or memory.
Source README
Elixir / Erlang: Idiomatic Efficiency Reference
When to Use
- Use this skill when the task matches this description: Language-specific super-code guidelines for elixir.
Table of Contents
- Pattern Matching & Guards
- Pipe Operator & Transforms
- Processes & OTP
- Error Handling
- Collections & Enum
- Structs & Protocols
- Anti-patterns specific to Elixir/Erlang
1. Pattern Matching & Guards {#patterns}
### ❌ Extracting with Map.get then checking
value = Map.get(map, :key)
if value != nil do
process(value)
end
### ✅ — pattern match directly
case map do
%{key: value} -> process(value)
_ -> :noop
end
### or with if:
if value = map[:key], do: process(value)
### ❌ Nested case for multiple conditions
case fetch_user(id) do
{:ok, user} ->
case validate(user) do
{:ok, valid_user} -> save(valid_user)
{:error, reason} -> {:error, reason}
end
{:error, reason} -> {:error, reason}
end
### ✅ — with clause
with {:ok, user} <- fetch_user(id),
{:ok, valid_user} <- validate(user) do
save(valid_user)
end
### ❌ if/else for known shapes
def area(shape) do
if shape.type == :circle do
:math.pi() * shape.radius * shape.radius
else
shape.width * shape.height
end
end
### ✅ — multi-clause function with pattern match
def area(%{type: :circle, radius: r}), do: :math.pi() * r * r
def area(%{type: :rect, width: w, height: h}), do: w * h
### ❌ Checking type at runtime
def process(x) do
if is_integer(x) and x > 0 do
x * 2
end
end
### ✅ — guard clause
def process(x) when is_integer(x) and x > 0, do: x * 2
def process(_), do: {:error, :invalid_input}
2. Pipe Operator & Transforms {#pipes}
### ❌ Nested function calls
String.trim(String.downcase(String.replace(input, ~r/\s+/, " ")))
### ✅
input
|> String.replace(~r/\s+/, " ")
|> String.downcase()
|> String.trim()
### ❌ Pipe into anonymous function awkwardly
data
|> (fn x -> x * 2 end).()
### ✅ — use then/1 or named function
data
|> then(&(&1 * 2))
### or better: extract a named function
data |> double()
### ❌ Single-step pipe (no gain in readability)
result = list |> Enum.count()
### ✅ — direct call for single operation
result = Enum.count(list)
Pipe when 2+ transforms. Direct call for single operation. First arg flows through pipe.
3. Processes & OTP {#otp}
### ❌ Raw spawn for stateful process
pid = spawn(fn -> loop(%{count: 0}) end)
send(pid, {:increment})
### ✅ — GenServer for stateful processes
defmodule Counter do
use GenServer
def start_link(init \\ 0), do: GenServer.start_link(__MODULE__, init)
def increment(pid), do: GenServer.call(pid, :increment)
@impl true
def init(count), do: {:ok, count}
@impl true
def handle_call(:increment, _from, count), do: {:reply, count + 1, count + 1}
end
### ❌ Spawning without linking (orphan process on crash)
spawn(fn -> do_work() end)
### ✅ — Task for fire-and-forget with supervision
Task.start(fn -> do_work() end)
### or for awaitable result:
task = Task.async(fn -> do_work() end)
result = Task.await(task)
### ❌ Manual process registry
Process.register(self(), :my_worker)
### ✅ — use Registry or named GenServer
{:ok, _} = Registry.start_link(keys: :unique, name: MyRegistry)
GenServer.start_link(Worker, arg, name: {:via, Registry, {MyRegistry, :my_worker}})
### ❌ try/catch in GenServer (breaks supervision)
def handle_call(:work, _from, state) do
try do
result = risky_operation()
{:reply, result, state}
catch
_ -> {:reply, :error, state}
end
end
### ✅ — let it crash; supervisor restarts
def handle_call(:work, _from, state) do
result = risky_operation()
{:reply, result, state}
end
"Let it crash" - supervisors handle recovery. Don't defensively catch inside GenServers.
4. Error Handling {#errors}
### ❌ Raising for expected failures
def find_user(id) do
case Repo.get(User, id) do
nil -> raise "User not found"
user -> user
end
end
### ✅ — tagged tuples for expected outcomes
def find_user(id) do
case Repo.get(User, id) do
nil -> {:error, :not_found}
user -> {:ok, user}
end
end
### ❌ Ignoring error tuple
{:ok, result} = might_fail() # crashes on {:error, _}
### ✅ — handle both cases
case might_fail() do
{:ok, result} -> process(result)
{:error, reason} -> Logger.error("Failed: #{inspect(reason)}")
end
### ❌ String errors
{:error, "something went wrong"}
### ✅ — atom or struct errors (matchable, cheap)
{:error, :timeout}
{:error, %ValidationError{field: :email, reason: :invalid_format}}
### ❌ Deep nesting of ok/error checks
case step1() do
{:ok, a} ->
case step2(a) do
{:ok, b} ->
case step3(b) do
{:ok, c} -> {:ok, c}
error -> error
end
error -> error
end
error -> error
end
### ✅
with {:ok, a} <- step1(),
{:ok, b} <- step2(a),
{:ok, c} <- step3(b) do
{:ok, c}
else
{:error, reason} -> {:error, reason}
end
5. Collections & Enum {#collections}
### ❌ Multiple passes when one suffices
items
|> Enum.filter(&(&1.active))
|> Enum.map(&(&1.name))
### ✅ — for comprehension when filter + transform
for %{active: true, name: name} <- items, do: name
### ❌ Enum.count for empty check (traverses whole list)
if Enum.count(list) == 0, do: :empty
### ✅
if Enum.empty?(list), do: :empty
### or pattern match:
case list do
[] -> :empty
_ -> :has_items
end
### ❌ Building map with Enum.reduce when Map.new works
Enum.reduce(users, %{}, fn user, acc -> Map.put(acc, user.id, user) end)
### ✅
Map.new(users, &{&1.id, &1})
### ❌ Enum on large dataset (eager — builds intermediate lists)
huge_list
|> Enum.map(&transform/1)
|> Enum.filter(&valid?/1)
|> Enum.take(10)
### ✅ — Stream for lazy evaluation
huge_list
|> Stream.map(&transform/1)
|> Stream.filter(&valid?/1)
|> Enum.take(10)
Use Stream when chaining transforms on large/infinite collections. Enum for small or final step.
6. Structs & Protocols {#structs}
### ❌ Plain map for domain entities
user = %{name: "Alice", email: "a@b.com", age: 30}
### typo in key goes unnoticed: user.emaail
### ✅ — struct enforces keys
defmodule User do
@enforce_keys [:name, :email]
defstruct [:name, :email, age: 0]
end
user = %User{name: "Alice", email: "a@b.com"}
### ❌ Protocol with only one implementation (over-abstraction)
defprotocol Renderable do
def render(data)
end
defimpl Renderable, for: HtmlPage do ... end
### ✅ — just a function until you need polymorphism
def render(%HtmlPage{} = page), do: ...
### ❌ Updating nested struct manually
updated = %{user | address: %{user.address | city: "NYC"}}
### ✅
updated = put_in(user.address.city, "NYC")
### or Kernel.update_in/3 for transforms
7. Anti-patterns specific to Elixir/Erlang {#antipatterns}
| Anti-pattern | Preferred |
|---|---|
spawn without link/monitor |
Task.start_link or GenServer |
try/catch inside GenServer |
let it crash; supervisor restarts |
| String error reasons | atom or struct errors |
Enum.count(x) == 0 |
Enum.empty?(x) or match?([], x) |
| Mutable-style accumulator | Enum.reduce / recursion |
if/else chain on data shape |
multi-clause function + pattern match |
Nested case for ok/error |
with expression |
IO.inspect left in prod |
Logger with levels |
| Single-step pipe | direct function call |
Enum on huge/infinite data |
Stream |
| Raw PID passing | named processes / Registry |
| Boolean returns for success/fail | {:ok, val} / {:error, reason} tuples |
length(list) > 0 (O(n)) |
pattern match `[_ |
| Shared mutable state via ETS without wrapper | GenServer or Agent as access layer |
Limitations
- These are language-specific guidelines and do not cover overall architectural decisions.
- Over-compression might reduce readability; apply judgement.
FAQ
Common questions
Discussion
Questions & comments · 0
Sign In Sign in to leave a comment.