functional-core-imperative-shell
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseFunctional Core, Imperative Shell (FCIS)
Functional Core, Imperative Shell (FCIS)
Overview
概述
Core principle: Separate pure business logic (Functional Core) from side effects (Imperative Shell). Pure functions go in one file, I/O operations in another.
Why this matters: Pure functions are trivial to test (no mocks needed). I/O code is isolated to thin shells. Bugs become structurally impossible when business logic has no side effects.
核心原则: 将纯业务逻辑(Functional Core)与副作用(Imperative Shell)分离。纯函数放在一个文件中,I/O 操作放在另一个文件中。
为什么这很重要: 纯函数极易测试(无需 mock)。I/O 代码被隔离在轻量级 Shell 中。当业务逻辑没有副作用时,bug 在结构上就不可能出现。
When to Use
使用时机
Use FCIS when:
- Writing any new code file
- Refactoring existing code
- Reviewing code for architectural decisions
- Deciding where logic belongs
Trigger symptoms:
- "Where should this function go?"
- Creating a new file
- Adding database calls to logic
- Adding file I/O to calculations
- Writing tests that need complex mocking
在以下情况使用 FCIS:
- 编写任何新的代码文件
- 重构现有代码
- 审查代码的架构决策
- 决定逻辑应放在哪里
触发信号:
- “这个函数应该放在哪里?”
- 创建新文件
- 在逻辑中添加数据库调用
- 在计算中添加文件 I/O
- 编写的测试需要复杂的 mock
MANDATORY: File Classification
强制要求:文件分类
YOU MUST add pattern comment to every file containing runtime behavior:
// pattern: Functional Core
// pattern: Imperative Shell
// pattern: Mixed (needs refactoring)If file genuinely cannot be separated (rare), document why:
// pattern: Mixed (unavoidable)
// Reason: [specific technical justification]
// Example: Performance-critical path where separating I/O causes unacceptable overheadNo file with runtime behavior without classification. If you create a file that contains functions, classes with methods, or orchestration logic without this comment, you have violated the requirement.
你必须为每个包含运行时行为的文件添加模式注释:
// pattern: Functional Core
// pattern: Imperative Shell
// pattern: Mixed (needs refactoring)如果文件确实无法分离(这种情况很少见),请记录原因:
// pattern: Mixed (unavoidable)
// Reason: [specific technical justification]
// Example: Performance-critical path where separating I/O causes unacceptable overhead任何包含运行时行为的文件都必须经过分类。 如果你创建了一个包含函数、带方法的类或编排逻辑的文件,却没有添加此注释,则违反了要求。
Exempt: Files Without Runtime Behavior
豁免:不包含运行时行为的文件
DO NOT add pattern comments to:
- Type-only files - files exporting only types, interfaces, or type aliases (no runtime code)
- Constants/enum-like files - static data declarations, no functions
- Barrel/index files - re-exports only ()
export * from './foo' - Test files - tests exercise core/shell code but aren't themselves core or shell
- Generated files - machine-generated code
- Bash/shell scripts (.sh, .bash) - inherently imperative
- Configuration files (eslint.config.js, tsconfig.json, .env, etc.)
- Markdown documentation (.md)
- HTML files (.html)
- Task runner files (justfile, Makefile, etc.)
- Package manifests (package.json, pyproject.toml, etc.)
- Data files (JSON, YAML, CSV, etc.)
Note: If an exempt file grows to include runtime logic (e.g., a "types" file gains helper functions, or a constants file gains factory functions), it crosses the threshold and MUST be classified.
Classification applies to application source files containing runtime behavior (functions with logic, classes with methods, I/O orchestration).
不要添加模式注释:
- 仅类型文件——仅导出类型、接口或类型别名(无运行时代码)的文件
- 常量/类枚举文件——仅含静态数据声明,不含函数
- Barrel/index 文件——仅执行再导出()
export * from './foo' - 测试文件——测试用于验证 Core/Shell 代码,但其自身既不是 Core 也不是 Shell
- 生成文件——机器生成的文件
- Bash/shell 脚本(.sh、.bash)——本质上就是命令式的
- 配置文件(eslint.config.js、tsconfig.json、.env 等)
- Markdown 文档(.md)
- HTML 文件(.html)
- 任务运行器文件(justfile、Makefile 等)
- 包清单(package.json、pyproject.toml 等)
- 数据文件(JSON、YAML、CSV 等)
注意: 如果某个豁免文件后来包含运行时逻辑(例如,“types”文件增加辅助函数,或“constants”文件增加工厂函数),它就超过了豁免阈值,必须进行分类。
分类规则适用于包含运行时行为的应用程序源文件(包含逻辑的函数、带方法的类、I/O 编排)。
File Type Definitions
文件类型定义
Functional Core Files
Functional Core 文件
Contains ONLY:
- Pure functions (same input -> same output, always)
- Business logic, validations, calculations, transformations
- Data structure operations
- Logging (EXCEPTION: loggers are permitted in Functional Core)
NEVER contains:
- File I/O (reading, writing files)
- Database operations (queries, updates, connections)
- HTTP requests or responses
- Environment variable access
- Date.now(), Math.random(), or other non-deterministic functions
- State mutations outside function scope
Logging exception: Functions MAY accept and use loggers. For unit tests, pass no-op loggers. This is the ONLY permitted side effect in Functional Core.
Test signature: Simple assertions, no mocks except logger (if used).
只能包含:
- 纯函数(相同输入 -> 相同输出,始终如此)
- 业务逻辑、校验、计算、转换
- 数据结构操作
- 日志记录(例外:Functional Core 中允许使用 logger)
绝不包含:
- 文件 I/O(读取、写入文件)
- 数据库操作(查询、更新、连接)
- HTTP 请求或响应
- 环境变量访问
- Date.now()、Math.random() 或其他非确定性函数
- 函数作用域之外的状态变更
日志记录例外: 函数可以接受并使用 logger。单元测试时,可传入 no-op logger。这是 Functional Core 中唯一允许的副作用。
测试特征: 简单断言,除 logger(使用时)外无需 mock。
Imperative Shell Files
Imperative Shell 文件
Contains ONLY:
- I/O operations: file system, database, HTTP, environment
- Orchestration: gather data -> call Functional Core -> persist results
- Error handling for I/O failures
- Minimal business logic (coordination only)
NEVER contains:
- Complex calculations
- Business rule validations
- Data transformations beyond format conversion
Test signature: Integration tests with real dependencies or test doubles.
只能包含:
- I/O 操作:文件系统、数据库、HTTP、环境
- 编排:收集数据 -> 调用 Functional Core -> 持久化结果
- I/O 失败的错误处理
- 最少量的业务逻辑(仅用于协调)
绝不包含:
- 复杂计算
- 业务规则校验
- 超出格式转换范围的数据转换
测试特征: 使用真实依赖或测试替身的集成测试。
Code Flow Pattern
代码流模式
1. GATHER (Shell): Collect data from external sources
2. PROCESS (Core): Transform input to output (pure)
3. PERSIST (Shell): Save results externallyEvery operation follows this sequence. No exceptions.
1. GATHER (Shell): Collect data from external sources
2. PROCESS (Core): Transform input to output (pure)
3. PERSIST (Shell): Save results externally每个操作都遵循此顺序,不例外。
Decision Framework
决策框架
Before writing a function, ask:
dot
digraph fcis_decision {
"Writing a function" [shape=ellipse];
"Can run without external dependencies?" [shape=diamond];
"Does it coordinate I/O?" [shape=diamond];
"Functional Core" [shape=box, style=filled, fillcolor=lightblue];
"Imperative Shell" [shape=box, style=filled, fillcolor=lightgreen];
"STOP: Refactor or escalate" [shape=octagon, style=filled, fillcolor=red, fontcolor=white];
"Writing a function" -> "Can run without external dependencies?";
"Can run without external dependencies?" -> "Functional Core" [label="yes"];
"Can run without external dependencies?" -> "Does it coordinate I/O?" [label="no"];
"Does it coordinate I/O?" -> "Imperative Shell" [label="yes"];
"Does it coordinate I/O?" -> "STOP: Refactor or escalate" [label="no"];
}Questions to ask:
- Can this logic run without file system, database, network, or environment?
- YES -> Functional Core
- NO -> Does it coordinate I/O or contain business logic?
- I/O coordination -> Imperative Shell
- Business logic + I/O -> STOP. Refactor or escalate to user.
在编写函数前,问:
- 这段逻辑能否在没有文件系统、数据库、网络或环境的情况下运行?
- 是 -> Functional Core
- 否 -> 它是否协调 I/O 或包含业务逻辑?
- I/O 协调 -> Imperative Shell
- 业务逻辑 + I/O -> 停止。重构或向用户升级处理。
Common Mistakes and Rationalizations
常见错误与合理化原因
| Excuse/Thought Pattern | Reality | What To Do |
|---|---|---|
| "Just one file read in this calculation" | File I/O = side effect. Not Functional Core. | Extract to Shell. Pass data as parameter. |
| "Database is passed as parameter, so it's pure" | Database operations are I/O. Not pure. | Move to Shell. Core receives data, not DB connection. |
| "This validation needs to check if file exists" | File system check = I/O. Not Functional Core. | Shell checks file, passes boolean to Core validation. |
| "Small HTTP call, won't hurt" | HTTP = side effect. Breaks purity guarantee. | Shell makes request, Core processes response data. |
| "Need Date.now() for timestamp calculation" | Non-deterministic. Not pure. | Shell passes timestamp as parameter. |
| "Logging is a side effect, should remove" | WRONG. Logging is explicitly permitted. | Keep logger. This is the exception. |
| "This function does both logic and I/O, but it's simpler" | Mixed concerns = untestable without mocks. | Split into Core (logic) + Shell (I/O). Test Core simply. |
| "File classification is overhead" | Prevents entire classes of bugs. Non-negotiable. | Add classification comment. Takes 10 seconds. |
| "I'll refactor later" | Later never comes. Do it now. | Classify and separate now. |
| "Performance requires mixing" | Prove it with benchmarks. Usually wrong. | Separate first. Optimize with evidence. Mark Mixed (unavoidable) with justification. |
| 借口/思维模式 | 实际情况 | 应采取的措施 |
|---|---|---|
| “只是在计算中读取一个文件” | 文件 I/O = 副作用。不属于 Functional Core。 | 提取到 Shell。将数据作为参数传入。 |
| “把数据库作为参数传入,所以它是纯函数” | 数据库操作是 I/O。不纯。 | 移到 Shell。Core 接收数据,而不是数据库连接。 |
| “这项校验需要检查文件是否存在” | 文件系统检查 = I/O。不属于 Functional Core。 | 由 Shell 检查文件,将布尔值传给 Core 校验。 |
| “只是一个小 HTTP 调用,不会造成影响” | HTTP = 副作用。会破坏纯粹性保证。 | 由 Shell 发起请求,Core 处理响应数据。 |
| “需要用 Date.now() 计算时间戳” | 非确定性。不纯。 | 由 Shell 传入时间戳。 |
| “日志记录是副作用,应该移除” | **错误。**明确允许记录日志。 | 保留 logger。这是例外。 |
| “这个函数同时执行逻辑和 I/O,但更简单” | 关注点混合 = 没有 mock 就难以测试。 | 拆分为 Core(逻辑)+ Shell(I/O)。简单测试 Core。 |
| “文件分类是额外开销” | 可防止整类 bug。不可妥协。 | 添加分类注释。只需 10 秒。 |
| “我稍后再重构” | 稍后永远不会到来。现在就做。 | 现在就分类并分离。 |
| “性能要求必须混合” | 用基准测试证明。通常是错误的。 | 先分离。基于证据优化。标记为 Mixed (unavoidable),并附理由。 |
Red Flags - STOP and Refactor
危险信号——停止并重构
If you catch yourself doing ANY of these, STOP:
- File I/O in a "pure" function (open, read, write, exists checks)
- Database passed as parameter to Functional Core (queries, updates, connections)
- HTTP requests in business logic (fetch, axios, requests)
- Environment variables in calculations (process.env, os.getenv)
- Math.random() or Date.now() in Functional Core (non-deterministic)
- Creating a file with runtime behavior without pattern classification comment
- Thinking "just this once" about mixing concerns
All of these mean: Extract I/O to Shell. Pass data to Core. Classify file correctly.
如果你发现自己出现以下任何情况,请停止:
- 在“纯”函数中进行文件 I/O(open、read、write、exists 检查)
- 将数据库作为参数传给 Functional Core(查询、更新、连接)
- 在业务逻辑中发出 HTTP 请求(fetch、axios、requests)
- 在计算中使用环境变量(process.env、os.getenv)
- 在 Functional Core 中使用 Math.random() 或 Date.now()(非确定性)
- 创建含运行时行为的文件,却未添加模式分类注释
- 抱着“就这一次”的想法混合关注点
所有这些情况都意味着: 将 I/O 提取到 Shell。将数据传给 Core。正确分类文件。
Implementation Patterns
实现模式
Functional Core Pattern
Functional Core 模式
python
undefinedpython
undefinedpattern: Functional Core
pattern: Functional Core
def calculate_total_with_tax(items, tax_rate, logger=None):
"""Pure calculation: same inputs always produce same output."""
if logger:
logger.debug(f"Calculating total for {len(items)} items")
subtotal = sum(item['price'] * item['quantity'] for item in items)
tax = subtotal * tax_rate
total = subtotal + tax
return {
'subtotal': subtotal,
'tax': tax,
'total': total
}
**No I/O. No database. No file system. Only computation.**def calculate_total_with_tax(items, tax_rate, logger=None):
"""Pure calculation: same inputs always produce same output."""
if logger:
logger.debug(f"Calculating total for {len(items)} items")
subtotal = sum(item['price'] * item['quantity'] for item in items)
tax = subtotal * tax_rate
total = subtotal + tax
return {
'subtotal': subtotal,
'tax': tax,
'total': total
}
**无 I/O。无数据库。无文件系统。只有计算。**Imperative Shell Pattern
Imperative Shell 模式
python
undefinedpython
undefinedpattern: Imperative Shell
pattern: Imperative Shell
def process_order(order_id, db, logger):
"""Orchestrates: gather -> process -> persist."""
# GATHER: Collect data from external sources
items = db.get_order_items(order_id)
tax_rate = db.get_tax_rate_for_order(order_id)
# PROCESS: Call Functional Core (pure logic)
result = calculate_total_with_tax(items, tax_rate, logger)
# PERSIST: Save results externally
db.update_order_total(order_id, result['total'])
return result
**Shell is thin. Core does heavy lifting. Testable separately.**def process_order(order_id, db, logger):
"""Orchestrates: gather -> process -> persist."""
# GATHER: Collect data from external sources
items = db.get_order_items(order_id)
tax_rate = db.get_tax_rate_for_order(order_id)
# PROCESS: Call Functional Core (pure logic)
result = calculate_total_with_tax(items, tax_rate, logger)
# PERSIST: Save results externally
db.update_order_total(order_id, result['total'])
return result
**Shell 很轻量。Core 承担主要工作。可以单独测试。**Mixed (Needs Refactoring) - Bad Example
Mixed(需要重构)——错误示例
python
undefinedpython
undefinedpattern: Mixed (needs refactoring)
pattern: Mixed (needs refactoring)
def calculate_and_save_total(order_id, db):
"""BAD: Mixes calculation with I/O. Hard to test."""
items = db.get_order_items(order_id) # I/O
subtotal = sum(item['price'] for item in items) # Logic
tax_rate = db.get_tax_rate_for_order(order_id) # I/O
tax = subtotal * tax_rate # Logic
total = subtotal + tax # Logic
db.update_order_total(order_id, total) # I/O
return total
**Testing this requires database mocks. Fragile. Refactor using patterns above.**def calculate_and_save_total(order_id, db):
"""BAD: Mixes calculation with I/O. Hard to test."""
items = db.get_order_items(order_id) # I/O
subtotal = sum(item['price'] for item in items) # Logic
tax_rate = db.get_tax_rate_for_order(order_id) # I/O
tax = subtotal * tax_rate # Logic
total = subtotal + tax # Logic
db.update_order_total(order_id, total) # I/O
return total
**测试此代码需要数据库 mock。十分脆弱。使用上述模式进行重构。**Logger Exception Details
Logger 例外详情
Loggers are EXPLICITLY PERMITTED in Functional Core.
python
undefined明确允许在 Functional Core 中使用 Logger。
python
undefinedpattern: Functional Core
pattern: Functional Core
def validate_order(order_data, logger=None):
"""Pure validation with logging."""
if logger:
logger.info(f"Validating order {order_data.get('id')}")
errors = []
if not order_data.get('items'):
errors.append("Order must have items")
if order_data.get('total', 0) < 0:
errors.append("Total cannot be negative")
if logger and errors:
logger.warning(f"Validation failed: {errors}")
return {'valid': len(errors) == 0, 'errors': errors}
**For unit tests:** Pass no-op logger or None. Function remains pure for testing.def validate_order(order_data, logger=None):
"""Pure validation with logging."""
if logger:
logger.info(f"Validating order {order_data.get('id')}")
errors = []
if not order_data.get('items'):
errors.append("Order must have items")
if order_data.get('total', 0) < 0:
errors.append("Total cannot be negative")
if logger and errors:
logger.warning(f"Validation failed: {errors}")
return {'valid': len(errors) == 0, 'errors': errors}
**对于单元测试:** 传入 no-op logger 或 None。函数仍保持纯粹,因而易于测试。Refactoring Patterns
重构模式
Common patterns for separating concerns:
分离关注点的常见模式:
Extract Pure Core from Impure Functions
从 impure 函数中提取纯 Core
Symptom: Function mixes I/O with logic
python
undefined症状: 函数将 I/O 与逻辑混在一起
python
undefinedBEFORE - hard to test
BEFORE - hard to test
def process_order(order_id: str) -> None:
order = db.fetch(order_id) # I/O
discount = calculate_discount(order) # Pure logic
total = apply_discount(order, discount) # Pure logic
db.save(order_id, total) # I/O
def process_order(order_id: str) -> None:
order = db.fetch(order_id) # I/O
discount = calculate_discount(order) # Pure logic
total = apply_discount(order, discount) # Pure logic
db.save(order_id, total) # I/O
AFTER - pure core extracted
AFTER - pure core extracted
def calculate_order_total(order: Order, rules: DiscountRules) -> Decimal:
"""Pure function - easy to test."""
discount = calculate_discount(order, rules)
return apply_discount(order, discount)
def process_order(order_id: str) -> None:
"""Thin I/O wrapper."""
order = db.fetch(order_id)
total = calculate_order_total(order, get_discount_rules())
db.save(order_id, total)
undefineddef calculate_order_total(order: Order, rules: DiscountRules) -> Decimal:
"""Pure function - easy to test."""
discount = calculate_discount(order, rules)
return apply_discount(order, discount)
def process_order(order_id: str) -> None:
"""Thin I/O wrapper."""
order = db.fetch(order_id)
total = calculate_order_total(order, get_discount_rules())
db.save(order_id, total)
undefinedReturn Values Instead of Mutating
返回值而非原地修改
Symptom: Methods mutate in place, making before/after comparison hard
python
undefined症状: 方法原地修改状态,导致难以比较修改前后
python
undefinedBEFORE - mutation
BEFORE - mutation
def sort_tasks(tasks: list[Task]) -> None:
tasks.sort(key=lambda t: t.priority)
def sort_tasks(tasks: list[Task]) -> None:
tasks.sort(key=lambda t: t.priority)
AFTER - returns new value
AFTER - returns new value
def sorted_tasks(tasks: list[Task]) -> list[Task]:
return sorted(tasks, key=lambda t: t.priority)
undefineddef sorted_tasks(tasks: list[Task]) -> list[Task]:
return sorted(tasks, key=lambda t: t.priority)
undefinedAdd Missing Inverse Operations
补充缺失的逆操作
Symptom: One-way operation exists but no inverse for testing roundtrips
python
undefined症状: 只存在单向操作,无法进行往返测试
python
undefinedBEFORE - only encode exists
BEFORE - only encode exists
def encode_message(msg: dict) -> bytes:
return msgpack.packb(msg)
def encode_message(msg: dict) -> bytes:
return msgpack.packb(msg)
AFTER - add decode for roundtrip testing
AFTER - add decode for roundtrip testing
def decode_message(data: bytes) -> dict:
return msgpack.unpackb(data)
undefineddef decode_message(data: bytes) -> dict:
return msgpack.unpackb(data)
undefinedReplace Hardcoded Dependencies
替换硬编码依赖
Symptom: Functions use globals or hardcoded config, can't test edge cases
python
undefined症状: 函数使用全局变量或硬编码配置,无法测试边界情况
python
undefinedBEFORE - uses global
BEFORE - uses global
def validate_input(data: str) -> bool:
return len(data) <= CONFIG.max_length
def validate_input(data: str) -> bool:
return len(data) <= CONFIG.max_length
AFTER - dependency injected
AFTER - dependency injected
def validate_input(data: str, max_length: int) -> bool:
return len(data) <= max_length
undefineddef validate_input(data: str, max_length: int) -> bool:
return len(data) <= max_length
undefinedRefactoring Priority
重构优先级
| Pattern | Impact | Effort | Priority |
|---|---|---|---|
| Extract pure core | HIGH | Medium | Do first |
| Add missing inverse | HIGH | Low | Quick win |
| Return instead of mutate | MEDIUM | Low | Easy improvement |
| Inject dependencies | MEDIUM | Medium | When testing blocked |
| 模式 | 影响 | 工作量 | 优先级 |
|---|---|---|---|
| 提取纯 Core | 高 | 中 | 优先处理 |
| 补充缺失的逆操作 | 高 | 低 | 快速收益 |
| 返回值而非修改 | 中 | 低 | 简单改进 |
| 注入依赖 | 中 | 中 | 测试受阻时 |
Refactoring Checklist
重构检查清单
When you find mixed concerns:
- Identify pure computations (logic, calculations, validations)
- Extract pure code to Functional Core file
- Identify I/O operations (file, database, HTTP, environment)
- Keep I/O in Imperative Shell file
- Shell gathers data, calls Core, persists results
- Add pattern classification comments to both files
- Test Core with simple assertions (no mocks except logger)
- Test Shell with integration tests
If you cannot separate: Escalate to user with specific technical justification. Don't assume mixed is necessary.
发现混合关注点时:
- 找出纯计算(逻辑、计算、校验)
- 将纯代码提取到 Functional Core 文件
- 找出 I/O 操作(文件、数据库、HTTP、环境)
- 将 I/O 保留在 Imperative Shell 文件中
- 由 Shell 收集数据、调用 Core 并持久化结果
- 为两个文件添加模式分类注释
- 使用简单断言测试 Core(除 logger 外无需 mock)
- 使用集成测试测试 Shell
如果无法分离: 向用户提供具体技术理由并升级处理。不要想当然地认为必须混合。
Summary
总结
FCIS in three rules:
- Functional Core: Pure functions only. No I/O except logging. Easy to test.
- Imperative Shell: I/O coordination only. Minimal logic. Calls Core.
- Classify every file with runtime behavior. Type-only files, constants, barrels, tests, and generated files are exempt.
When in doubt: Can it run without external dependencies? -> Functional Core. Otherwise -> Imperative Shell.
Logging exception: Loggers permitted everywhere. Pass no-op logger for unit tests.
Mixed concerns = refactoring needed. Extract, separate, classify. Do it now, not later.
FCIS 的三条规则:
- Functional Core: 仅包含纯函数。除日志记录外,不得有 I/O。易于测试。
- Imperative Shell: 仅协调 I/O。逻辑最少。调用 Core。
- 对每个包含运行时行为的文件进行分类。 仅类型文件、常量文件、Barrel 文件、测试文件和生成文件可豁免。
如有疑问: 它能否在没有外部依赖的情况下运行?-> Functional Core。否则 -> Imperative Shell。
日志记录例外: 允许在各处使用 logger。单元测试时传入 no-op logger。
混合关注点 = 需要重构。 提取、分离、分类。现在就做,不要拖延。