Loading...
Loading...
Use when writing or refactoring code, before creating files - enforces separation of pure business logic (Functional Core) from side effects (Imperative Shell) using FCIS pattern with mandatory file classification
npx skill4agent add ed3dai/ed3d-plugins functional-core-imperative-shell// 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 overheadexport * from './foo'1. GATHER (Shell): Collect data from external sources
2. PROCESS (Core): Transform input to output (pure)
3. PERSIST (Shell): Save results externallydigraph 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"];
}| 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. |
# 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
}# 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# 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# 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}# 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
# 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)# BEFORE - mutation
def sort_tasks(tasks: list[Task]) -> None:
tasks.sort(key=lambda t: t.priority)
# AFTER - returns new value
def sorted_tasks(tasks: list[Task]) -> list[Task]:
return sorted(tasks, key=lambda t: t.priority)# BEFORE - only encode exists
def encode_message(msg: dict) -> bytes:
return msgpack.packb(msg)
# AFTER - add decode for roundtrip testing
def decode_message(data: bytes) -> dict:
return msgpack.unpackb(data)# BEFORE - uses global
def validate_input(data: str) -> bool:
return len(data) <= CONFIG.max_length
# AFTER - dependency injected
def validate_input(data: str, max_length: int) -> bool:
return len(data) <= max_length| 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 |