| name | functional-programming |
| description | Apply functional programming principles to write predictable, testable code. Outputs pure function patterns, immutability strategies, composition pipelines, and FP-style refactoring examples. |
| argument-hint | ["language","codebase style","team FP experience","areas to apply FP"] |
| allowed-tools | Read, Write |
Functional Programming
Functional programming (FP) treats computation as the evaluation of mathematical functions. The core properties — pure functions, immutability, and function composition — make code easier to test, reason about, and parallelise. You don't need a functional language to apply FP principles; they improve code in any language.
Core Principles
PURE FUNCTIONS
Same inputs always produce same outputs
No side effects (no mutation, no I/O, no state changes)
Referentially transparent: can replace with its return value
IMMUTABILITY
Data is never modified after creation
Create new values instead of changing existing ones
Prevents shared mutable state bugs
FUNCTION COMPOSITION
Build complex behaviour from small, composable functions
Data flows through a pipeline of transformations
HIGHER-ORDER FUNCTIONS
Functions that take or return other functions
map, filter, reduce, compose, curry
Pure Functions
total_discount = 0
def apply_discount(price: float, pct: float) -> float:
global total_discount
discount = price * pct
total_discount += discount
return price - discount
def apply_discount(price: float, pct: float) -> tuple[float, float]:
discount = price * pct
return price - discount, discount
prices = [100.0, 200.0, 50.0]
discounted, discounts = zip(*[apply_discount(p, 0.10) for p in prices])
total_discount = sum(discounts)
Immutability
from dataclasses import dataclass, replace
from typing import FrozenSet
class Order:
def __init__(self, items, status):
self.items = items
self.status = status
def add_item(order, item):
order.items.append(item)
@dataclass(frozen=True)
class Order:
items: tuple
status: str
customer_id: str
def add_item(order: Order, item: str) -> Order:
return replace(order, items=order.items + (item,))
def confirm(order: Order) -> Order:
return replace(order, status="confirmed")
order = Order(items=(), status="draft", customer_id="cust-1")
order = add_item(order, "widget")
order = add_item(order, "gadget")
order = confirm(order)
Function Composition
from functools import reduce
from typing import Callable, TypeVar
T = TypeVar("T")
def compose(*fns: Callable) -> Callable:
"""Right-to-left composition: compose(f, g, h)(x) == f(g(h(x)))"""
return reduce(lambda f, g: lambda x: f(g(x)), fns)
def pipe(*fns: Callable) -> Callable:
"""Left-to-right composition: pipe(f, g, h)(x) == h(g(f(x)))"""
return reduce(lambda f, g: lambda x: g(f(x)), fns)
def strip_whitespace(s: str) -> str: return s.strip()
def to_lowercase(s: str) -> str: return s.lower()
def remove_punctuation(s: str) -> str:
import re; return re.sub(r'[^\w\s]', '', s)
def split_words(s: str) -> list[str]: s.split()
normalize_text = pipe(
strip_whitespace,
to_lowercase,
remove_punctuation,
split_words,
)
result = normalize_text()
Higher-Order Functions
from typing import Callable
def multiply(x: float) -> Callable[[float], float]:
return lambda y: x * y
double = multiply(2)
triple = multiply(3)
prices = [10.0, 20.0, 30.0]
doubled_prices = list(map(double, prices))
def memoize(fn: Callable) -> Callable:
cache = {}
def wrapper(*args):
if args not in cache:
cache[args] = fn(*args)
return cache[args]
return wrapper
@memoize
def fibonacci(n: int) -> int:
if n <= 1: return n
return fibonacci(n - 1) + fibonacci(n - 2)
orders = [
{"id": 1, "amount": 50.0, : },
{: , : , : },
{: , : , : },
{: , : , : },
]
total_paid = reduce(
acc, o: acc + o[],
( o: o[] == , orders),
)
Functors and Monads (Optional / Result Types)
from typing import Generic, TypeVar, Callable, Optional
from dataclasses import dataclass
T = TypeVar("T")
E = TypeVar("E")
U = TypeVar("U")
@dataclass
class Result(Generic[T]):
"""
Represent either a success value or an error.
Eliminates try/except chains in pure functional code.
"""
_value: Optional[T] = None
_error: Optional[Exception] = None
@classmethod
def ok(cls, value: T) -> "Result[T]":
return cls(_value=value)
@classmethod
def err(cls, error: Exception) -> "Result[T]":
return cls(_error=error)
@property
def is_ok(self) -> bool:
return self._error is None
def map(self, fn: Callable[[T], U]) -> "Result[U]":
"""Apply fn to value if ok; propagate error otherwise."""
if self.is_ok:
try:
Result.ok(fn(._value))
Exception e:
Result.err(e)
Result.err(._error)
() -> :
.is_ok:
fn(._value)
Result.err(._error)
() -> T:
._value .is_ok default
() -> Result[]:
:
Result.ok((s))
ValueError:
Result.err(ValueError())
() -> Result[]:
amount > :
Result.ok(amount)
Result.err(ValueError())
() -> Result[]:
Result.ok(amount * )
final = (parse_amount()
.flat_map(validate_positive)
.flat_map(apply_tax))
(final.unwrap())
bad = parse_amount().flat_map(validate_positive)
(bad.is_ok)
Refactoring to FP Style
def process_orders(orders):
results = []
for order in orders:
if order['status'] == 'paid':
total = 0
for item in order['items']:
total += item['price'] * item['quantity']
if total > 100:
results.append({
'order_id': order['id'],
'total': total,
'discount': total * 0.1
})
return results
def calculate_total(order: dict) -> float:
return sum(item['price'] * item['quantity'] for item in order['items'])
def is_paid(order: dict) -> bool:
return order['status'] == 'paid'
def is_high_value(total: float) -> bool:
return total >
() -> :
{: order[], : total, : total * }
() -> :
paid_orders = (is_paid, orders)
with_totals = ((o, calculate_total(o)) o paid_orders)
high_value = ((o, t) o, t with_totals is_high_value(t))
[to_summary(o, t) o, t high_value]
Anti-Patterns to Avoid
| Anti-Pattern | Problem | Fix |
|---|
| Functions with hidden state | Same input, different output — untestable | Move state to parameters or return values |
| Mutating function arguments | Callers' data changes unexpectedly | Return new values; use copy() or frozen types |
| Overusing exceptions for control flow | Exceptions are side effects | Use Result/Option types for expected failure cases |
| Deep nesting instead of composition | Hard to read, test each step separately | Extract named functions; compose them |
| FP for everything | I/O, databases, and UI are inherently imperative | Apply FP to pure business logic; accept imperative at boundaries |
10 Rules
- Pure functions first — if a function can be pure, make it pure.
- Immutable data by default — return new values instead of mutating.
- Side effects at the boundaries — I/O, state, external calls belong at the edge of your system.
- Small, named functions compose better than large anonymous lambdas.
- map/filter/reduce over imperative loops for transformations.
- Result/Option types eliminate try/except chains in pure logic.
- Function composition creates readable pipelines without temporary variables.
- Test pure functions in isolation — no mocks, no setup, just inputs and outputs.
- Don't force FP on teams unfamiliar with it — introduce incrementally through pure functions.
- FP and OOP coexist — use FP for transformations, OOP for modelling.