Skip to content
Backend
Skill

/elixir-thinking

This skill should be used when the user asks to "implement a feature in Elixir", "refactor this module", "should I use a GenServer here?", "how should I structure this?", "use the pipe operator", "add error handling", "make this concurrent", or mentions protocols, behaviours,

From plugin
claude-code-elixir
1676 skills
Install
$ npx -y skills add georgeguimaraes/claude-code-elixir --skill elixir-thinking --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/elixir-thinking

Context preview

The summary Claude sees to decide when to auto-load this skill.

This skill should be used when the user asks to "implement a feature in Elixir", "refactor this module", "should I use a GenServer here?", "how should I structure this?", "use the pipe operator", "add error handling", "make this concurrent", or mentions protocols, behaviours,

SKILL.md

elixir-thinking.SKILL.md
name: elixir-thinking
description: This skill should be used when the user asks to "implement a feature in Elixir", "refactor this module", "should I use a GenServer here?", "how should I structure this?", "use the pipe operator", "add error handling", "make this concurrent", or mentions protocols, behaviours, pattern matching, with statements, comprehensions, structs, or coming from an OOP background. Contains paradigm-shifting insights.

Elixir Thinking

Mental shifts required before writing Elixir. These contradict conventional OOP patterns.

The Iron Law

NO PROCESS WITHOUT A RUNTIME REASON

Before creating a GenServer, Agent, or any process, answer YES to at least one: 1. Do I need mutable state persisting across calls? 2. Do I need concurrent execution? 3. Do I need fault isolation?

**All three are NO?** Use plain functions. Modules organize code; processes manage runtime.

The Three Decoupled Dimensions

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.

"Let It Crash" = "Let It Heal"

The misconception: Write careless code. The truth: Supervisors START processes.

  • Handle expected errors explicitly (`{:ok, _}` / `{:error, _}`)
  • Let unexpected errors crash → supervisor restarts

Control Flow

**Pattern matching first:**

  • Match on function heads instead of `if/else` or `case` in bodies
  • `%{}` matches ANY map—use `map_size(map) == 0` guard for empty maps
  • Avoid nested `case`—refactor to single `case`, `with`, or separate functions

**Error handling:**

  • Use `{:ok, result}` / `{:error, reason}` for operations that can fail
  • Avoid raising exceptions for control flow
  • Use `with` for chaining `{:ok, _}` / `{:error, _}` operations

**Be explicit about expected cases:**

  • Avoid `_ -> nil` catch-alls—they silently swallow unexpected cases
  • Avoid `value && value.field` nil-punning—obscures actual return types
  • When a case has `{: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

Polymorphism

| 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.

Data Modeling Replaces Class Hierarchies

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: ...

Defaults and Options

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

Idioms

  • Process dictionary is typically unidiomatic—pass state explicitly
  • Reserve `is_thing` names for guards only
  • Use structs over maps when shape is known: `defstruct [:name, :age]`
  • Prepend to lists `[new | list]` not `list ++ [new]`
  • Use `dbg/1` for debugging—prints formatted value with context
  • Use built-in `JSON` module (Elixir 1.18+) instead of Jason

Testing

**Always 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

Same goes for type-only predicates: `assert is_map(user)` / `assert is_list(posts)` pass for almost any non-error return. Pattern match the shape and content together: `assert %User{email: "a@b.com"} = user`. `is_nil/1` is fine when nil-ness is the whole point.

**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 uni

Read more
Ships withclaude-code-elixir

Claude Code plugins for Elixir development.

Get the whole plugin
Stats
168
Stars
15
Forks
Maintained
Maintenance
Shell
Language
Apache-2.0
License
3mo ago
Last commit
7mo ago
Created

Repo: georgeguimaraes/claude-code-elixir

Other skills on claude-code-elixir.