elixir-thinking

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Elixir Thinking

Elixir 编程思维

Mental shifts required before writing Elixir. These contradict conventional OOP patterns.
编写Elixir代码前需要转变的思维模式,这些与传统OOP模式相悖。

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.
NO PROCESS WITHOUT A RUNTIME REASON
在创建GenServer、Agent或任何进程之前,至少要对以下一个问题回答“是”:
  1. 我需要跨调用持久化的可变状态吗?
  2. 我需要并发执行吗?
  3. 我需要故障隔离吗?
三个问题都回答“否”? 使用纯函数。模块用于组织代码;进程用于管理运行时。

The Three Decoupled Dimensions

三个解耦维度

OOP couples behavior, state, and mutability together. Elixir decouples them:
OOP DimensionElixir Equivalent
BehaviorModules (functions)
StateData (structs, maps)
MutabilityProcesses (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
    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:
elixir
undefined
优先使用模式匹配:
  • 在函数头部进行匹配,而非在函数体内使用
    if/else
    case
  • %{}
    匹配任意map——使用
    map_size(map) == 0
    守卫判断空map
  • 避免嵌套
    case
    ——重构为单个
    case
    with
    或拆分函数
错误处理:
  • 对可能失败的操作使用
    {:ok, result}
    /
    {:error, reason}
    格式
  • 避免用异常处理控制流
  • 使用
    with
    链式处理
    {:ok, _}
    /
    {:error, _}
    操作
明确预期场景:
  • 避免使用
    _ -> nil
    这种兜底分支——它们会静默吞掉意外场景
  • 避免
    value && value.field
    这种空值判断——会模糊实际返回类型
  • 当场景中同时存在
    {:ok, nil} -> nil
    {:ok, value} -> value.field
    时,优先使用
    with
elixir
undefined

Verbose

冗余写法

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
undefined
with {:ok, %{recommendations: recs}} <- get_run(id), do: recs
undefined

Polymorphism

多态

For Polymorphism Over...UseContract
ModulesBehaviorsUpfront callbacks
DataProtocolsUpfront implementations
ProcessesMessage passingImplicit (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
/3
variants (
Keyword.get/3
,
Map.get/3
) instead of case statements branching on
nil
:
elixir
undefined
使用
/3
变体函数(
Keyword.get/3
Map.get/3
)替代基于
nil
分支的case语句:
elixir
undefined

WRONG

错误写法

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:

```elixir
Keyword.get(opts, :chunker, :default) |> parse_chunker_config()

不要创建辅助函数合并配置默认值,直接内联回退逻辑:

```elixir

WRONG

错误写法

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
undefined
def some_function(opts) do repo = opts[:repo] || Application.get_env(:app, :repo) end
undefined

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
  • 进程字典通常不符合惯用写法——显式传递状态
  • 仅在守卫中使用
    is_thing
    命名的函数
  • 当数据结构已知时,优先使用structs而非maps:
    defstruct [:name, :age]
  • 向列表添加元素时使用
    [new | list]
    而非
    list ++ [new]
  • 使用
    dbg/1
    调试——打印带上下文的格式化值
  • 使用内置
    JSON
    模块(Elixir 1.18+)替代Jason

Testing

测试

Prefer pattern matching over imperative assertions. Never use
assert length
+
Enum.at
/
List.last
/
hd
. Pattern match checks length and content in one shot:
elixir
undefined
优先使用模式匹配而非命令式断言。永远不要用
assert length
+
Enum.at
/
List.last
/
hd
。模式匹配可同时检查长度和内容:
elixir
undefined

Bad

不佳写法

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"
end
Directory is auto-cleaned before each run. Works with
@moduletag :tmp_dir
for all tests in module.
assert [%{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_dir
为模块内所有测试启用该功能。

Common Rationalizations

常见自我合理化借口

ExcuseReality
"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
出现以上任意情况?重新阅读铁律。