elixir-thinking
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseElixir Thinking
Elixir 编程思维
Mental shifts required before writing Elixir. These contradict conventional OOP patterns.
编写Elixir代码前需要转变的思维模式,这些与传统OOP模式相悖。
The Iron Law
铁律
NO PROCESS WITHOUT A RUNTIME REASONBefore creating a GenServer, Agent, or any process, answer YES to at least one:
- Do I need mutable state persisting across calls?
- Do I need concurrent execution?
- Do I need fault isolation?
All three are NO? Use plain functions. Modules organize code; processes manage runtime.
NO PROCESS WITHOUT A RUNTIME REASON在创建GenServer、Agent或任何进程之前,至少要对以下一个问题回答“是”:
- 我需要跨调用持久化的可变状态吗?
- 我需要并发执行吗?
- 我需要故障隔离吗?
三个问题都回答“否”? 使用纯函数。模块用于组织代码;进程用于管理运行时。
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.
OOP将行为、状态和可变性耦合在一起。Elixir则将它们解耦:
| OOP维度 | Elixir对应机制 |
|---|---|
| 行为 | 模块(函数) |
| 状态 | 数据(structs、maps) |
| 可变性 | 进程(GenServer) |
选择你实际需要的部分。“我只需要数据和函数”意味着不需要进程。
"Let It Crash" = "Let It Heal"
"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
误区:编写粗心的代码。
真相:Supervisor负责重启进程。
- 显式处理预期错误(/
{:ok, _}){:error, _} - 让意外错误崩溃 → 由supervisor重启
Control Flow
控制流
Pattern matching first:
- Match on function heads instead of or
if/elsein bodiescase - matches ANY map—use
%{}guard for empty mapsmap_size(map) == 0 - Avoid nested —refactor to single
case,case, or separate functionswith
Error handling:
- Use /
{:ok, result}for operations that can fail{:error, reason} - Avoid raising exceptions for control flow
- Use for chaining
with/{:ok, _}operations{:error, _}
Be explicit about expected cases:
- Avoid catch-alls—they silently swallow unexpected cases
_ -> nil - Avoid nil-punning—obscures actual return types
value && value.field - When a case has alongside
{:ok, nil} -> nil, use{:ok, value} -> value.fieldinstead:with
elixir
undefined优先使用模式匹配:
- 在函数头部进行匹配,而非在函数体内使用或
if/elsecase - 匹配任意map——使用
%{}守卫判断空mapmap_size(map) == 0 - 避免嵌套——重构为单个
case、case或拆分函数with
错误处理:
- 对可能失败的操作使用/
{:ok, result}格式{:error, reason} - 避免用异常处理控制流
- 使用链式处理
with/{:ok, _}操作{:error, _}
明确预期场景:
- 避免使用这种兜底分支——它们会静默吞掉意外场景
_ -> nil - 避免这种空值判断——会模糊实际返回类型
value && value.field - 当场景中同时存在和
{:ok, nil} -> nil时,优先使用{:ok, value} -> value.field:with
elixir
undefinedVerbose
冗余写法
case get_run(id) do
{:ok, nil} -> nil
{:ok, run} -> run.recommendations
end
case get_run(id) do
{:ok, nil} -> nil
{:ok, run} -> run.recommendations
end
Prefer
推荐写法
with {:ok, %{recommendations: recs}} <- get_run(id), do: recs
undefinedwith {:ok, %{recommendations: recs}} <- get_run(id), do: recs
undefinedPolymorphism
多态
| 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.
| 针对...实现多态 | 使用方式 | 约定 |
|---|---|---|
| 模块 | Behaviors | 预先定义回调 |
| 数据 | Protocols | 预先实现逻辑 |
| 进程 | 消息传递 | 隐式(send/receive) |
Behaviors = 模块多态的默认选择(运行时开销极低)
Protocols = 仅在组合数据类型时使用,尤其是内置类型
消息传递 = 仅在设计上需要状态时使用(IO、文件句柄)
使用最简单的抽象:模式匹配 → 匿名函数 → behaviors → protocols → 消息传递。每一步都会增加复杂度。
合理使用场景: 库的扩展性、多实现支持、测试替换需求。
保持耦合的场景: 内部模块、单一实现、模式匹配可覆盖所有情况。
Data Modeling Replaces Class Hierarchies
数据建模替代类层级
OOP: Complex class hierarchy + visitor pattern.
Elixir: Model as data + pattern matching + recursion.
elixir
{: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: ...OOP:复杂类层级 + 访问者模式。
Elixir:数据建模 + 模式匹配 + 递归。
elixir
{: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 variants (, ) instead of case statements branching on :
/3Keyword.get/3Map.get/3nilelixir
undefined使用变体函数(、)替代基于分支的case语句:
/3Keyword.get/3Map.get/3nilelixir
undefinedWRONG
错误写法
case Keyword.get(opts, :chunker) do
nil -> chunker()
config -> parse_chunker_config(config)
end
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:
```elixirKeyword.get(opts, :chunker, :default) |> parse_chunker_config()
不要创建辅助函数合并配置默认值,直接内联回退逻辑:
```elixirWRONG
错误写法
defp merge_defaults(opts), do: Keyword.merge([repo: Application.get_env(:app, :repo)], opts)
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
undefineddef some_function(opts) do
repo = opts[:repo] || Application.get_env(:app, :repo)
end
undefinedIdioms
惯用写法
- Process dictionary is typically unidiomatic—pass state explicitly
- Reserve names for guards only
is_thing - Use structs over maps when shape is known:
defstruct [:name, :age] - Prepend to lists not
[new | list]list ++ [new] - Use for debugging—prints formatted value with context
dbg/1 - Use built-in module (Elixir 1.18+) instead of Jason
JSON
- 进程字典通常不符合惯用写法——显式传递状态
- 仅在守卫中使用命名的函数
is_thing - 当数据结构已知时,优先使用structs而非maps:
defstruct [:name, :age] - 向列表添加元素时使用而非
[new | list]list ++ [new] - 使用调试——打印带上下文的格式化值
dbg/1 - 使用内置模块(Elixir 1.18+)替代Jason
JSON
Testing
测试
Prefer pattern matching over imperative assertions. Never use + //. Pattern match checks length and content in one shot:
assert lengthEnum.atList.lasthdelixir
undefined优先使用模式匹配而非命令式断言。永远不要用 + //。模式匹配可同时检查长度和内容:
assert lengthEnum.atList.lasthdelixir
undefinedBad
不佳写法
assert length(students) == 2
assert Enum.at(students, 0).name == "Alice"
assert Enum.at(students, 1).name == "Bob"
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:
```elixir
@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"
endDirectory is auto-cleaned before each run. Works with for all tests in module.
@moduletag :tmp_dirassert [%{name: "Alice"}, %{name: "Bob"}] = students
**测试行为而非实现**。测试用例/公开API。重构不应导致测试失败。
**测试你的代码而非框架**。如果删除你的代码后测试仍不失败,说明测试是冗余的。
**保持测试异步**。`async: false`意味着你耦合了全局状态。修复耦合问题:
| 问题 | 解决方案 |
|------|----------|
| `Application.put_env` | 将配置作为函数参数传递 |
| 功能开关 | 通过进程字典或上下文注入 |
| ETS表 | 为每个测试创建唯一命名的表 |
| 外部API | 使用Mox并显式设置允许列表 |
| 文件系统操作 | 使用`@tag :tmp_dir`(见下文) |
**文件测试使用`tmp_dir`**。ExUnit会为每个测试创建唯一的临时目录,支持异步:
```elixir
@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目录会在每次运行前自动清理。可使用为模块内所有测试启用该功能。
@moduletag :tmp_dirCommon Rationalizations
常见自我合理化借口
| 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. |
| 借口 | 真相 |
|---|---|
| "我需要用进程来组织代码" | 模块用于组织代码。进程是为运行时服务的。 |
| "GenServer是Elixir的标准写法" | 纯函数也是Elixir的标准写法。 |
| "我最终会需要状态" | YAGNI(你不会用到它)。需要时再添加进程。 |
| "这只是一个简单的包装进程" | 简单包装会逐渐成为性能瓶颈。 |
| "我在OOP里就是这么构建的" | 从数据流角度重新思考。 |
Red Flags - STOP and Reconsider
危险信号 - 立即停止并重新考虑
- Creating process without answering the three questions
- Using GenServer for stateless operations
- Wrapping a library in a process "for safety"
- One process per entity without runtime justification
- Reaching for protocols when pattern matching works
Any of these? Re-read The Iron Law.
- 创建进程但未回答三个核心问题
- 对无状态操作使用GenServer
- 为“安全”起见将库包装进进程
- 为每个实体创建进程但无运行时层面的理由
- 当模式匹配可解决问题时仍使用protocols
出现以上任意情况?重新阅读铁律。