Pure function — same input always gives same output, zero side effects
Concept
Pure function — a function that:
- Same input → same output: Given the same arguments, always returns the same value. No randomness, no reading external state.
- No side effects: Does not modify anything outside itself. No DB writes, no logging, no global state changes.
Why pure functions are valuable:
- Trivially testable: Just call with inputs, assert outputs. No mocking, no setup, no teardown.
- Memoizable: Safe to cache the result keyed by inputs — calling again with same inputs gives same result.
- Composable: Chain pure functions together. The output of one is the input of the next. Like Unix pipes.
- Refactorable: Move, rename, extract — won't break other code because there are no hidden dependencies.
- Parallelizable: No shared state → no race conditions.
Contrast with impure functions: Functions that read from the database, get the current time (time()), generate random numbers, read from a config file, or write logs are impure. Their output depends on external state.
PHP and purity: PHP doesn't enforce purity (unlike Haskell). But you can write pure functions by discipline — don't read $_GET, don't write to a file, don't use static/global variables.
Functional programming in PHP: array_map(), array_filter(), array_reduce() — these are higher-order functions designed to work with pure functions. usort($arr, fn($a, $b) => $a['age'] <=> $b['age']) — the comparison function is pure.
"Functional core, imperative shell": Architecture pattern. The core of your domain logic is pure. The shell (controllers, commands, event handlers) is where side effects happen. The core is the most testable part.
Code Example
<?php
// PURE FUNCTIONS — same input, same output, no side effects
// ✅ Pure — only depends on its arguments
function add(int $a, int $b): int { return $a + $b; }
// ✅ Pure — deterministic, no external state
function celsiusToFahrenheit(float $celsius): float { return $celsius * 9/5 + 32; }
// ✅ Pure — no side effects, same output for same input
function applyDiscount(float $price, float $discountPercent): float
{
return $price * (1 - $discountPercent / 100);
}
// ✅ Pure — creates new array, doesn't mutate input
function filterAdults(array $users): array
{
return array_values(array_filter($users, fn($u) => $u['age'] >= 18));
}
// ❌ IMPURE — reads external state (current time)
function isExpired(int $expiresAt): bool { return $expiresAt < time(); } // time() changes!
// Fix: pass the time as a parameter
function isExpiredAt(int $expiresAt, int $now): bool { return $expiresAt < $now; } // pure!
// ❌ IMPURE — reads database
function getUserName(int $id): string
{
return \DB::table('users')->where('id', $id)->value('name'); // DB call!
}
// Fix: pass the name as a parameter
// ❌ IMPURE — modifies external state
function createOrderAndEmail(array $data): array
{
$order = Order::create($data); // DB write!
Mail::to($data['email'])->send(new OrderConfirmation($order)); // email!
return $order->toArray();
}
// FUNCTIONAL CORE example
class PricingEngine // pure functions only
{
public function applyDiscounts(float $price, array $discounts): float
{
return array_reduce($discounts, fn($p, $d) => $p * (1 - $d / 100), $price);
}
public function calculateTax(float $subtotal, float $rate): float
{
return round($subtotal * $rate, 2);
}
public function buildOrderSummary(array $items, float $taxRate, array $discounts): array
{
$subtotal = array_sum(array_column($items, 'price'));
$discounted = $this->applyDiscounts($subtotal, $discounts);
$tax = $this->calculateTax($discounted, $taxRate);
return ['subtotal' => $subtotal, 'discounted' => $discounted, 'tax' => $tax, 'total' => $discounted + $tax];
}
}
// Test purely — no mocking at all
$engine = new PricingEngine();
$summary = $engine->buildOrderSummary([['price' => 100], ['price' => 50]], 0.10, [10]); // 10% discount
assert($summary['discounted'] === 135.0);
assert($summary['total'] === 148.5);