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对应项
行为模块(函数)
状态数据(结构体、映射)
可变性进程(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
    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_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
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
    这类命名
  • 当数据结构已知时,优先使用结构体而非映射:
    defstruct [:name, :age]
  • 向列表添加元素时使用前置
    [new | list]
    而非后置
    list ++ [new]
  • 使用
    dbg/1
    进行调试——会打印带上下文的格式化值
  • 使用内置的
    JSON
    模块(Elixir 1.18+)替代Jason

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.
async: false
means you've coupled to global state. Fix the coupling:
ProblemSolution
Application.put_env
Pass config as function argument
Feature flagsInject via process dictionary or context
ETS tablesCreate per-test tables with unique names
External APIsUse Mox with explicit allowances
File system operationsUse
@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.
测试行为,而非实现。 测试用例/公开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
出现以上任何一种情况?重新阅读铁律。