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对应项 |
|---|---|
| 行为 | 模块(函数) |
| 状态 | 数据(结构体、映射) |
| 可变性 | 进程(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, _} - 让意外错误触发崩溃→由监督者重启进程
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_size(map) == 0 - 避免嵌套——重构为单个
case、case或拆分到不同函数中with
错误处理:
- 对可能失败的操作使用/
{:ok, result}格式{:error, reason} - 避免将异常用于控制流
- 使用来链式处理
with/{:ok, _}操作{:error, _}
明确预期场景:
- 避免使用这种全匹配分支——它们会静默忽略意外场景
_ -> nil - 避免这种空值判断方式——会模糊实际返回类型
value && value.field - 当某个case分支同时包含和
{: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 - 当数据结构已知时,优先使用结构体而非映射:
defstruct [:name, :age] - 向列表添加元素时使用前置而非后置
[new | list]list ++ [new] - 使用进行调试——会打印带上下文的格式化值
dbg/1 - 使用内置的模块(Elixir 1.18+)替代Jason
JSON
Testing
测试
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. means you've coupled to global state. Fix the coupling:
async: false| Problem | Solution |
|---|---|
| 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 |
Use for file tests. ExUnit creates unique temp directories per test, async-safe:
tmp_direlixir
@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_dir测试行为,而非实现。 测试用例/公开API。重构不应导致测试失败。
测试你的代码,而非框架。 如果删除你的代码后测试仍不失败,那这个测试是无意义的。
保持测试异步。 意味着你已经耦合到了全局状态。修复耦合问题:
async: false| 问题 | 解决方案 |
|---|---|
| 将配置作为函数参数传递 |
| 功能开关 | 通过进程字典或上下文注入 |
| ETS表 | 创建每个测试独有的表,使用唯一名称 |
| 外部API | 使用Mox并显式设置允许列表 |
| 文件系统操作 | 使用 |
文件测试使用。 ExUnit会为每个测试创建唯一的临时目录,支持异步:
tmp_direlixir
@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
出现以上任何一种情况?重新阅读铁律。