functional-core-imperative-shell

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Functional 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 overhead
No 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 externally
Every 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 PatternRealityWhat 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
undefined
python
undefined

pattern: 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
undefined
python
undefined

pattern: 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
undefined
python
undefined

pattern: 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
undefined

pattern: 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
undefined

BEFORE - 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)
undefined
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)
undefined

Return Values Instead of Mutating

返回值而非原地修改

Symptom: Methods mutate in place, making before/after comparison hard
python
undefined
症状: 方法原地修改状态,导致难以比较修改前后
python
undefined

BEFORE - 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)
undefined
def sorted_tasks(tasks: list[Task]) -> list[Task]: return sorted(tasks, key=lambda t: t.priority)
undefined

Add Missing Inverse Operations

补充缺失的逆操作

Symptom: One-way operation exists but no inverse for testing roundtrips
python
undefined
症状: 只存在单向操作,无法进行往返测试
python
undefined

BEFORE - 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)
undefined
def decode_message(data: bytes) -> dict: return msgpack.unpackb(data)
undefined

Replace Hardcoded Dependencies

替换硬编码依赖

Symptom: Functions use globals or hardcoded config, can't test edge cases
python
undefined
症状: 函数使用全局变量或硬编码配置,无法测试边界情况
python
undefined

BEFORE - 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
undefined
def validate_input(data: str, max_length: int) -> bool: return len(data) <= max_length
undefined

Refactoring Priority

重构优先级

PatternImpactEffortPriority
Extract pure coreHIGHMediumDo first
Add missing inverseHIGHLowQuick win
Return instead of mutateMEDIUMLowEasy improvement
Inject dependenciesMEDIUMMediumWhen 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:
  1. Functional Core: Pure functions only. No I/O except logging. Easy to test.
  2. Imperative Shell: I/O coordination only. Minimal logic. Calls Core.
  3. 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 的三条规则:
  1. Functional Core: 仅包含纯函数。除日志记录外,不得有 I/O。易于测试。
  2. Imperative Shell: 仅协调 I/O。逻辑最少。调用 Core。
  3. 对每个包含运行时行为的文件进行分类。 仅类型文件、常量文件、Barrel 文件、测试文件和生成文件可豁免。
如有疑问: 它能否在没有外部依赖的情况下运行?-> Functional Core。否则 -> Imperative Shell。
日志记录例外: 允许在各处使用 logger。单元测试时传入 no-op logger。
混合关注点 = 需要重构。 提取、分离、分类。现在就做,不要拖延。