Skill v1.0.1
currentAutomated scan100/100+4 new
name: elixir description: "Language-specific super-code guidelines for elixir." risk: safe source: community date_added: "2026-06-16" metadata: version: "1.0.1"
Elixir / Erlang: Idiomatic Efficiency Reference
Apply only within the requested scope and current project conventions. Preserve behavior, ownership, error handling, accessibility and security; examples are independent fragments, not equivalent rewrites in every context. Verify language/library versions and run relevant checks. Required tests and user-facing progress/results take precedence over brevity rules. Do not install dependencies or delete files merely to follow these examples.
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 checkingvalue = Map.get(map, :key)if value != nil doprocess(value)end# ✅ — pattern match directlycase map do%{key: value} when not is_nil(value) -> process(value)_ -> :noopend# or with if:if value = map[:key], do: process(value)
# ❌ Nested case for multiple conditionscase 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 clausewith {:ok, user} <- fetch_user(id),{:ok, valid_user} <- validate(user) dosave(valid_user)end
# ❌ if/else for known shapesdef area(shape) doif shape.type == :circle do:math.pi() * shape.radius * shape.radiuselseshape.width * shape.heightendend# ✅ — multi-clause function with pattern matchdef area(%{type: :circle, radius: r}), do: :math.pi() * r * rdef area(%{type: :rect, width: w, height: h}), do: w * h
# ❌ Checking type at runtimedef process(x) doif is_integer(x) and x > 0 dox * 2endend# ✅ — guard clausedef process(x) when is_integer(x) and x > 0, do: x * 2def process(_), do: {:error, :invalid_input}
2. Pipe Operator & Transforms {#pipes}
# ❌ Nested function callsString.trim(String.downcase(String.replace(input, ~r/\s+/, " ")))# ✅input|> String.replace(~r/\s+/, " ")|> String.downcase()|> String.trim()
# ❌ Pipe into anonymous function awkwardlydata|> (fn x -> x * 2 end).()# ✅ — use then/1 or named functiondata|> then(&(&1 * 2))# or better: extract a named functiondata |> double()
# ❌ Single-step pipe (no gain in readability)result = list |> Enum.count()# ✅ — direct call for single operationresult = 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 processpid = spawn(fn -> loop(%{count: 0}) end)send(pid, {:increment})# ✅ — GenServer for stateful processesdefmodule Counter douse GenServerdef start_link(init \\ 0), do: GenServer.start_link(__MODULE__, init)def increment(pid), do: GenServer.call(pid, :increment)@impl truedef init(count), do: {:ok, count}@impl truedef 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 supervisionTask.Supervisor.start_child(MyTaskSupervisor, fn -> do_work() end) # supervisor must already be started# or for awaitable result:task = Task.async(fn -> do_work() end)result = Task.await(task)
# ❌ Manual process registryProcess.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) dotry doresult = risky_operation(){:reply, result, state}catch_ -> {:reply, :error, state}endend# ✅ — let it crash; supervisor restartsdef handle_call(:work, _from, state) doresult = risky_operation(){:reply, result, state}end
Use supervisors for unexpected failures; handle expected domain failures explicitly. Restarts do not roll back external effects, so require idempotency and recovery planning.
4. Error Handling {#errors}
# ❌ Raising for expected failuresdef find_user(id) docase Repo.get(User, id) donil -> raise "User not found"user -> userendend# ✅ — tagged tuples for expected outcomesdef find_user(id) docase Repo.get(User, id) donil -> {:error, :not_found}user -> {:ok, user}endend
# ❌ Ignoring error tuple{:ok, result} = might_fail() # crashes on {:error, _}# ✅ — handle both casescase 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 checkscase step1() do{:ok, a} ->case step2(a) do{:ok, b} ->case step3(b) do{:ok, c} -> {:ok, c}error -> errorenderror -> errorenderror -> errorend# ✅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 sufficesitems|> Enum.filter(&(&1.active))|> Enum.map(&(&1.name))# ✅ — for comprehension when filter + transformfor %{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_itemsend
# ❌ Building map with Enum.reduce when Map.new worksEnum.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 evaluationhuge_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 entitiesuser = %{name: "Alice", email: "a@b.com", age: 30}# typo in key goes unnoticed: user.emaail# ✅ — struct enforces keysdefmodule User do@enforce_keys [:name, :email]defstruct [:name, :email, age: 0]enduser = %User{name: "Alice", email: "a@b.com"}
# ❌ Protocol with only one implementation (over-abstraction)defprotocol Renderable dodef render(data)enddefimpl Renderable, for: HtmlPage do ... end# ✅ — just a function until you need polymorphismdef render(%HtmlPage{} = page), do: ...
# ❌ Updating nested struct manuallyupdated = %{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.