Write pure functions and avoid side effects for predictable, testable code
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 100from 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 valuePure 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 manipulationThe guidance above uses JavaScript for the primary examples. For the same patterns in other languages and deeper material, see:
pantheon-org/tekhne
c8416e9
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.