skills/software-engineering/fp-pure-functions/SKILL.md
Write pure functions and avoid side effects for predictable, testable code
npx skillsauth add pantheon-org/tekhne fp-pure-functionsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Pure functions are the foundation of functional programming. A pure function is a function where the return value is determined only by its input values, without observable side effects. This predictability makes code easier to test, reason about, and parallelize.
A pure function must satisfy two key properties:
// IMPURE: Depends on external state
let discount = 0.1;
function calculatePrice(price) {
return price - (price * discount);
}
// PURE: All inputs are parameters
function calculatePriceWithDiscount(price, discount) {
return price - (price * discount);
}
// IMPURE: Modifies external state
let total = 0;
function addToTotal(amount) {
total += amount;
return total;
}
// PURE: Returns new value without mutation
function add(a, b) {
return a + b;
}
// Usage of pure function
const currentTotal = 100;
const newTotal = add(currentTotal, 50); // 150
// currentTotal is still 100
from datetime import datetime
from typing import List, Dict
# IMPURE: Uses current time (non-deterministic)
def get_greeting():
hour = datetime.now().hour
if hour < 12:
return "Good morning"
return "Good afternoon"
# PURE: Time is passed as parameter
def get_greeting_at_time(hour: int) -> str:
if hour < 12:
return "Good morning"
return "Good afternoon"
# IMPURE: Modifies input list
def add_item_impure(items: List[str], item: str) -> List[str]:
items.append(item)
return items
# PURE: Returns new list
def add_item_pure(items: List[str], item: str) -> List[str]:
return [*items, item]
# IMPURE: Reads from file system
def load_config():
with open('config.json', 'r') as f:
return json.load(f)
# PURE: Config is passed as parameter
def process_config(config: Dict) -> Dict:
return {
**config,
'processed': True,
'timestamp': config.get('timestamp', 0)
}
When side effects are necessary, isolate them at the boundaries:
// PURE CORE: Business logic
function calculateOrderTotal(items, taxRate, shippingCost) {
const subtotal = items.reduce((sum, item) =>
sum + (item.price * item.quantity), 0
);
const tax = subtotal * taxRate;
return subtotal + tax + shippingCost;
}
function validateOrder(order, inventory) {
const errors = [];
for (const item of order.items) {
const stock = inventory[item.id];
if (!stock || stock < item.quantity) {
errors.push(`Insufficient stock for ${item.name}`);
}
}
if (order.items.length === 0) {
errors.push('Order must contain at least one item');
}
return {
valid: errors.length === 0,
errors
};
}
function createInvoice(order, total) {
return {
orderId: order.id,
customerId: order.customerId,
items: order.items,
total,
createdAt: order.timestamp
};
}
// IMPERATIVE SHELL: Side effects isolated here
async function processOrder(orderId) {
// Side effect: Database read
const order = await db.orders.findById(orderId);
const inventory = await db.inventory.getAll();
// Pure function call
const validation = validateOrder(order, inventory);
if (!validation.valid) {
// Side effect: Logging
console.error('Order validation failed:', validation.errors);
return { success: false, errors: validation.errors };
}
// Pure function call
const total = calculateOrderTotal(
order.items,
order.taxRate,
order.shippingCost
);
// Pure function call
const invoice = createInvoice(order, total);
// Side effects: Database writes
await db.invoices.create(invoice);
await db.orders.update(orderId, { status: 'processed' });
// Side effect: Email
await emailService.send(order.customerEmail, invoice);
return { success: true, invoice };
}
from dataclasses import dataclass
from typing import List, Dict, Callable
from datetime import datetime
@dataclass
class Order:
id: str
items: List[Dict]
customer_id: str
tax_rate: float
shipping_cost: float
# PURE: All dependencies are parameters
def calculate_order_total(
items: List[Dict],
tax_rate: float,
shipping_cost: float
) -> float:
subtotal = sum(item['price'] * item['quantity'] for item in items)
tax = subtotal * tax_rate
return subtotal + tax + shipping_cost
def validate_order(order: Order, inventory: Dict[str, int]) -> Dict:
errors = []
for item in order.items:
stock = inventory.get(item['id'], 0)
if stock < item['quantity']:
errors.append(f"Insufficient stock for {item['name']}")
if not order.items:
errors.append('Order must contain at least one item')
return {
'valid': len(errors) == 0,
'errors': errors
}
def create_invoice(order: Order, total: float, timestamp: datetime) -> Dict:
return {
'order_id': order.id,
'customer_id': order.customer_id,
'items': order.items,
'total': total,
'created_at': timestamp.isoformat()
}
# IMPURE: But dependencies injected for testability
class OrderProcessor:
def __init__(self, db, email_service, logger, clock: Callable[[], datetime]):
self.db = db
self.email_service = email_service
self.logger = logger
self.clock = clock # Inject time dependency
async def process_order(self, order_id: str) -> Dict:
# Side effect: Database read
order = await self.db.orders.find_by_id(order_id)
inventory = await self.db.inventory.get_all()
# Pure function call
validation = validate_order(order, inventory)
if not validation['valid']:
# Side effect: Logging (injected)
self.logger.error(f"Order validation failed: {validation['errors']}")
return {'success': False, 'errors': validation['errors']}
# Pure function calls
total = calculate_order_total(
order.items,
order.tax_rate,
order.shipping_cost
)
# Time is injected, making this testable
invoice = create_invoice(order, total, self.clock())
# Side effects: Database writes
await self.db.invoices.create(invoice)
await self.db.orders.update(order_id, {'status': 'processed'})
# Side effect: Email
await self.email_service.send(order.customer_email, invoice)
return {'success': True, 'invoice': invoice}
A pure function exhibits referential transparency: you can replace a function call with its return value without changing program behavior.
// Pure function
function add(a, b) {
return a + b;
}
// These are equivalent due to referential transparency
const result1 = add(2, 3) * add(4, 5);
const result2 = 5 * 9; // Can replace function calls with values
const result3 = 45;
// Impure function (random)
function getRandomNumber() {
return Math.random();
}
// These are NOT equivalent
const value1 = getRandomNumber() + getRandomNumber();
const value2 = 0.5 + 0.5; // Wrong! Can't replace with specific value
Pure functions are trivial to test:
import pytest
from decimal import Decimal
# Pure function
def calculate_compound_interest(
principal: Decimal,
rate: Decimal,
years: int,
compounds_per_year: int
) -> Decimal:
return principal * (1 + rate / compounds_per_year) ** (compounds_per_year * years)
# Tests are simple - no mocking needed
def test_compound_interest_basic():
result = calculate_compound_interest(
principal=Decimal('1000'),
rate=Decimal('0.05'),
years=1,
compounds_per_year=12
)
assert result == pytest.approx(Decimal('1051.16'), rel=1e-2)
def test_compound_interest_zero_rate():
result = calculate_compound_interest(
principal=Decimal('1000'),
rate=Decimal('0'),
years=5,
compounds_per_year=4
)
assert result == Decimal('1000')
def test_compound_interest_multiple_years():
result = calculate_compound_interest(
principal=Decimal('5000'),
rate=Decimal('0.06'),
years=10,
compounds_per_year=4
)
assert result == pytest.approx(Decimal('9070.09'), rel=1e-2)
# No need for:
# - Database setup/teardown
# - Mocking external services
# - Clearing global state
# - Time manipulation
The guidance above uses JavaScript for the primary examples. For the same patterns in other languages and deeper material, see:
tools
A skill that produces warnings but no errors.
testing
A well-formed example skill for testing the validator.
development
A skill with code blocks and imperative instructions for testing content and contamination analysis.
tools