✍️ 필사 모드: Functional Programming Guide for Backend Developers 2025: Immutability, Monads, Composition, Side Effect Management
EnglishTable of Contents
1. Why Backend Developers Need Functional Programming
Functional programming (FP) is not an academic curiosity but a practical weapon in modern backend development.
Why FP shines in backend development:
- Testability — pure functions can be verified with just inputs and outputs, no mocking needed
- Concurrency safety — immutable data can be safely shared across threads without locks
- Maintainability — composition of small functions makes code readable and changeable
- Error handling — Either/Result monads enable safe error handling without exceptions
- Data pipelines — map/filter/reduce express data transformations declaratively
FP does not replace OOP. Combining both paradigms appropriately is the modern approach.
Functional Programming Core Concepts
Foundations
├── Pure Function
├── Immutability
├── First-class Function
└── Higher-order Function
Composition
├── Function Composition
├── Currying
├── Partial Application
└── Point-free Style
Types and Patterns
├── Algebraic Data Types (ADT)
├── Pattern Matching
├── Lens
└── Optic
Monads and Containers
├── Functor (map)
├── Applicative (ap)
├── Monad (flatMap/bind)
├── Maybe/Option (null safety)
├── Either/Result (error handling)
└── IO (side effect isolation)
2. Pure Functions
2.1 Definition
A pure function satisfies two conditions:
- Same input always yields same output (deterministic)
- No side effects (does not modify external state)
// Pure function
function add(a: number, b: number): number {
return a + b; // Always same result, no side effects
}
function formatPrice(amount: number, currency: string): string {
return `${currency} ${amount.toFixed(2)}`;
}
// Impure — depends on external state
let taxRate = 0.1;
function calculateTax(amount: number): number {
return amount * taxRate; // Result changes if taxRate changes
}
// Impure — has side effects
function saveUser(user: User): void {
database.save(user); // External state mutation
console.log("User saved"); // I/O side effect
analytics.track("signup"); // External service call
}
2.2 Referential Transparency
A pure function call can be replaced with its result without changing the program's meaning. This is referential transparency.
// Referentially transparent
const x = add(3, 4); // 7
const y = add(3, 4); // 7
// Replacing all add(3, 4) with 7 doesn't change behavior
// NOT referentially transparent
const now1 = Date.now(); // 1713000000000
const now2 = Date.now(); // 1713000000001
// Cannot replace Date.now() with a specific value
2.3 Memoization
Since pure functions guarantee same output for same input, results can be cached.
// TypeScript Memoization
function memoize<Args extends unknown[], R>(
fn: (...args: Args) => R,
): (...args: Args) => R {
const cache = new Map<string, R>();
return (...args: Args): R => {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key)!;
}
const result = fn(...args);
cache.set(key, result);
return result;
};
}
function fibonacci(n: number): number {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const memoFib = memoize(fibonacci);
console.log(memoFib(40)); // First call: slow
console.log(memoFib(40)); // Second: instant (cached)
# Python Memoization
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n: int) -> int:
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
3. Immutability
3.1 Why Immutability Matters
Mutable state is the primary source of bugs. It is difficult to track when, where, and by whom data is being changed.
// Mutable — dangerous
const users = [{ name: "Alice", age: 30 }, { name: "Bob", age: 25 }];
function birthday(user: { name: string; age: number }) {
user.age += 1; // Mutates the original!
return user;
}
birthday(users[0]);
// users[0].age is now 31 — unexpected side effect
// Immutable — safe
function birthdayImmutable(user: { name: string; age: number }) {
return { ...user, age: user.age + 1 }; // Creates new object
}
const olderAlice = birthdayImmutable(users[0]);
// users[0].age is still 30 — original preserved
3.2 Immutability in TypeScript
// Readonly utility types
interface Config {
readonly host: string;
readonly port: number;
readonly features: ReadonlyArray<string>;
}
const config: Config = {
host: "localhost",
port: 8080,
features: ["auth", "logging"],
};
// config.port = 9090; // Compile error!
// config.features.push("cache"); // Compile error!
// DeepReadonly — deep immutability
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
// Immutable update pattern
interface State {
user: { name: string; preferences: { theme: string; lang: string } };
items: Array<{ id: number; name: string }>;
}
function updateTheme(state: State, newTheme: string): State {
return {
...state,
user: {
...state.user,
preferences: {
...state.user.preferences,
theme: newTheme,
},
},
};
}
3.3 Structural Sharing
Immutable data does not copy everything on each update. Unchanged parts share references with the previous version.
// Immer.js — immutable updates as easy as mutation
import { produce } from "immer";
interface AppState {
users: Array<{
id: string;
name: string;
settings: { notifications: boolean; theme: string };
}>;
metadata: { lastUpdated: string; version: number };
}
const initialState: AppState = {
users: [
{ id: "1", name: "Alice", settings: { notifications: true, theme: "light" } },
{ id: "2", name: "Bob", settings: { notifications: false, theme: "dark" } },
],
metadata: { lastUpdated: "2025-01-01", version: 1 },
};
const newState = produce(initialState, (draft) => {
const alice = draft.users.find((u) => u.id === "1");
if (alice) {
alice.settings.theme = "dark";
}
draft.metadata.lastUpdated = "2025-04-14";
draft.metadata.version += 1;
});
// initialState is unchanged
// newState.users[1] === initialState.users[1] // true (structural sharing)
// newState.users[0] !== initialState.users[0] // true (changed part is new)
3.4 Immutability in Python
# Python — frozen dataclass
from dataclasses import dataclass, replace
@dataclass(frozen=True)
class User:
name: str
email: str
age: int
alice = User(name="Alice", email="alice@example.com", age=30)
# alice.age = 31 # FrozenInstanceError!
older_alice = replace(alice, age=31)
print(alice.age) # 30 (original preserved)
print(older_alice.age) # 31 (new object)
# NamedTuple — immutable tuple
from typing import NamedTuple
class Point(NamedTuple):
x: float
y: float
z: float = 0.0
p1 = Point(1.0, 2.0)
p2 = p1._replace(x=3.0) # Creates new Point
# frozenset — immutable set
tags = frozenset(["python", "fp", "backend"])
new_tags = tags | frozenset(["new"]) # Creates new frozenset
3.5 Immutability in Rust
// Rust — immutable by default
fn main() {
let x = 5;
// x = 6; // Compile error! Variables are immutable by default
let mut y = 5; // Explicitly mutable with mut keyword
y = 6; // OK
// Ownership + Immutability = Concurrency safety
let data = vec![1, 2, 3];
let sum = calculate_sum(&data); // Immutable reference
println!("Sum: {sum}");
}
fn calculate_sum(numbers: &[i32]) -> i32 {
numbers.iter().sum()
}
4. Higher-Order Functions
A higher-order function takes functions as arguments or returns a function.
4.1 map / filter / reduce
// TypeScript — Data transformation pipeline
interface Transaction {
id: string;
amount: number;
type: "credit" | "debit";
category: string;
date: string;
}
const transactions: Transaction[] = [
{ id: "1", amount: 100, type: "credit", category: "salary", date: "2025-04-01" },
{ id: "2", amount: 50, type: "debit", category: "food", date: "2025-04-02" },
{ id: "3", amount: 200, type: "credit", category: "freelance", date: "2025-04-03" },
{ id: "4", amount: 30, type: "debit", category: "transport", date: "2025-04-04" },
{ id: "5", amount: 80, type: "debit", category: "food", date: "2025-04-05" },
];
// Declarative data processing
const totalFoodExpense = transactions
.filter((t) => t.type === "debit")
.filter((t) => t.category === "food")
.map((t) => t.amount)
.reduce((sum, amount) => sum + amount, 0);
console.log(`Total food expense: ${totalFoodExpense}`); // 130
// Aggregation by category
const byCategory = transactions
.filter((t) => t.type === "debit")
.reduce<Record<string, number>>((acc, t) => {
acc[t.category] = (acc[t.category] || 0) + t.amount;
return acc;
}, {});
console.log(byCategory); // { food: 130, transport: 30 }
4.2 Functions Returning Functions
// Validation function factory
type Validator<T> = (value: T) => string | null;
function minLength(min: number): Validator<string> {
return (value: string) =>
value.length >= min ? null : `Must be at least ${min} characters`;
}
function maxLength(max: number): Validator<string> {
return (value: string) =>
value.length <= max ? null : `Must be at most ${max} characters`;
}
function matches(pattern: RegExp, message: string): Validator<string> {
return (value: string) =>
pattern.test(value) ? null : message;
}
function composeValidators<T>(...validators: Validator<T>[]): Validator<T> {
return (value: T) => {
for (const validator of validators) {
const error = validator(value);
if (error) return error;
}
return null;
};
}
// Usage
const validatePassword = composeValidators(
minLength(8),
maxLength(128),
matches(/[A-Z]/, "Must contain uppercase letter"),
matches(/[0-9]/, "Must contain a number"),
matches(/[!@#$%^&*]/, "Must contain special character"),
);
console.log(validatePassword("weak")); // "Must be at least 8 characters"
console.log(validatePassword("Strong1!x")); // null (valid)
# Python — Higher-order functions
from typing import Callable
def compose(*funcs: Callable) -> Callable:
"""Compose functions right to left"""
def composed(x):
result = x
for f in reversed(funcs):
result = f(result)
return result
return composed
def pipe(*funcs: Callable) -> Callable:
"""Compose functions left to right"""
def piped(x):
result = x
for f in funcs:
result = f(result)
return result
return piped
def double(x: int) -> int: return x * 2
def add_ten(x: int) -> int: return x + 10
def to_string(x: int) -> str: return f"Result: {x}"
transform = pipe(double, add_ten, to_string)
print(transform(5)) # "Result: 20"
5. Function Composition
5.1 pipe and compose
Connecting small functions to build complex logic is the essence of functional programming.
// TypeScript — pipe utility
function pipe<A, B>(a: A, ab: (a: A) => B): B;
function pipe<A, B, C>(a: A, ab: (a: A) => B, bc: (b: B) => C): C;
function pipe<A, B, C, D>(a: A, ab: (a: A) => B, bc: (b: B) => C, cd: (c: C) => D): D;
function pipe(initial: unknown, ...fns: Function[]): unknown {
return fns.reduce((acc, fn) => fn(acc), initial);
}
// Data transformation pipeline
interface RawUser {
first_name: string;
last_name: string;
email_address: string;
birth_date: string;
}
interface ProcessedUser {
fullName: string;
email: string;
age: number;
isAdult: boolean;
}
const normalizeNames = (user: RawUser) => ({
...user,
first_name: user.first_name.trim().toLowerCase(),
last_name: user.last_name.trim().toLowerCase(),
});
const buildFullName = (user: RawUser) => ({
fullName: `${user.first_name} ${user.last_name}`,
email: user.email_address.toLowerCase(),
birthDate: user.birth_date,
});
const calculateAge = (user: { fullName: string; email: string; birthDate: string }) => {
const birth = new Date(user.birthDate);
const age = Math.floor(
(Date.now() - birth.getTime()) / (365.25 * 24 * 60 * 60 * 1000),
);
return { fullName: user.fullName, email: user.email, age };
};
const addAdultFlag = (user: { fullName: string; email: string; age: number }): ProcessedUser => ({
...user,
isAdult: user.age >= 18,
});
const rawUser: RawUser = {
first_name: " JOHN ",
last_name: " DOE ",
email_address: "John.Doe@Example.COM",
birth_date: "1990-05-15",
};
const processed = pipe(rawUser, normalizeNames, buildFullName, calculateAge, addAdultFlag);
5.2 Point-free Style
A style where function arguments are not explicitly mentioned in the definition.
// Pointful (explicit arguments)
const doubledEvens1 = (numbers: number[]) =>
numbers.filter((n) => n % 2 === 0).map((n) => n * 2);
// Near point-free
const isEven = (n: number) => n % 2 === 0;
const double = (n: number) => n * 2;
const doubledEvens2 = (numbers: number[]) =>
numbers.filter(isEven).map(double);
6. Currying and Partial Application
6.1 Currying
Transforms a multi-argument function into a chain of single-argument functions.
// TypeScript Currying
function addCurried(a: number): (b: number) => (c: number) => number {
return (b: number) => (c: number) => a + b + c;
}
const add5 = addCurried(5); // (b) => (c) => 5 + b + c
const add5and3 = add5(3); // (c) => 5 + 3 + c
const result = add5and3(2); // 10
// Practical: API request builder
const request = (method: string) => (baseUrl: string) => (path: string) =>
fetch(`${baseUrl}${path}`, { method });
const get = request("GET");
const apiGet = get("https://api.example.com");
// Reusable specialized functions
await apiGet("/users");
await apiGet("/products");
await apiGet("/orders");
6.2 Partial Application
Fixes some arguments of a function to create a new function.
# Python — functools.partial
from functools import partial
def log(level: str, module: str, message: str) -> str:
return f"[{level}] [{module}] {message}"
error_log = partial(log, "ERROR")
auth_error = partial(error_log, "AUTH")
db_error = partial(error_log, "DB")
print(auth_error("Invalid token")) # [ERROR] [AUTH] Invalid token
print(db_error("Connection timeout")) # [ERROR] [DB] Connection timeout
# Practical: data transformation pipeline
def multiply(factor: float, value: float) -> float:
return value * factor
def format_currency(symbol: str, value: float) -> str:
return f"{symbol}{value:,.2f}"
to_usd = partial(format_currency, "$")
apply_tax = partial(multiply, 1.1)
apply_discount = partial(multiply, 0.85)
price = 100.0
final = to_usd(apply_tax(apply_discount(price)))
print(final) # $93.50
7. Algebraic Data Types (ADT)
7.1 Sum Types — Union/Enum
A type that can be one of several possible forms.
// TypeScript — Discriminated Union
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
}
}
// Result type
type ApiResult<T> =
| { status: "success"; data: T; timestamp: number }
| { status: "error"; error: string; code: number }
| { status: "loading" };
function handleResult<T>(result: ApiResult<T>): string {
switch (result.status) {
case "success":
return `Data received: ${JSON.stringify(result.data)}`;
case "error":
return `Error ${result.code}: ${result.error}`;
case "loading":
return "Loading...";
}
}
# Python — match (3.10+)
from dataclasses import dataclass
@dataclass
class Circle:
radius: float
@dataclass
class Rectangle:
width: float
height: float
@dataclass
class Triangle:
base: float
height: float
Shape = Circle | Rectangle | Triangle
def area(shape: Shape) -> float:
match shape:
case Circle(radius=r):
return 3.14159 * r ** 2
case Rectangle(width=w, height=h):
return w * h
case Triangle(base=b, height=h):
return b * h / 2
// Rust — enum (true algebraic data types)
enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
Triangle { base: f64, height: f64 },
}
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
Shape::Rectangle { width, height } => width * height,
Shape::Triangle { base, height } => base * height / 2.0,
}
}
8. Option/Maybe Monad — Null Safety
8.1 The Problem with null
Missing null/undefined checks cause runtime errors. Tony Hoare called null his "billion-dollar mistake."
8.2 Maybe/Option Monad
// TypeScript — Maybe Monad Implementation
class Maybe<T> {
private constructor(private readonly value: T | null) {}
static of<T>(value: T | null | undefined): Maybe<T> {
return new Maybe(value ?? null);
}
static none<T>(): Maybe<T> {
return new Maybe<T>(null);
}
isNone(): boolean {
return this.value === null;
}
map<U>(fn: (value: T) => U): Maybe<U> {
if (this.isNone()) return Maybe.none<U>();
return Maybe.of(fn(this.value!));
}
flatMap<U>(fn: (value: T) => Maybe<U>): Maybe<U> {
if (this.isNone()) return Maybe.none<U>();
return fn(this.value!);
}
getOrElse(defaultValue: T): T {
return this.isNone() ? defaultValue : this.value!;
}
match<U>(patterns: { some: (value: T) => U; none: () => U }): U {
return this.isNone() ? patterns.none() : patterns.some(this.value!);
}
}
// Usage — clean null-safe chaining
interface User {
name: string;
address?: { city?: string; zipCode?: string };
}
function findUser(id: string): Maybe<User> {
const users: Record<string, User> = {
"1": { name: "Alice", address: { city: "Seoul", zipCode: "06000" } },
"2": { name: "Bob" },
};
return Maybe.of(users[id] || null);
}
const city = findUser("1")
.flatMap((user) => Maybe.of(user.address))
.map((address) => address.city)
.getOrElse("Unknown");
console.log(city); // "Seoul"
const unknownCity = findUser("999")
.flatMap((user) => Maybe.of(user.address))
.map((address) => address.city)
.getOrElse("Unknown");
console.log(unknownCity); // "Unknown" — no error!
// Rust — Option is built into the language
fn find_user(id: &str) -> Option<User> {
// ...
}
fn get_user_city(id: &str) -> String {
find_user(id)
.and_then(|user| user.address)
.map(|addr| addr.city)
.unwrap_or_else(|| "Unknown".to_string())
}
// More concise with ? operator
fn get_user_city_short(id: &str) -> Option<String> {
let user = find_user(id)?;
let address = user.address?;
Some(address.city)
}
9. Either/Result Monad — Error Handling Without Exceptions
9.1 The Problem with Exceptions
Exceptions make code flow unpredictable. The type system cannot guarantee which function throws which exception.
9.2 Either/Result Implementation
// TypeScript — Either Monad
type Either<L, R> = Left<L> | Right<R>;
class Left<L> {
readonly _tag = "Left" as const;
constructor(readonly value: L) {}
map<R2>(_fn: (value: never) => R2): Either<L, R2> {
return this as unknown as Either<L, R2>;
}
flatMap<R2>(_fn: (value: never) => Either<L, R2>): Either<L, R2> {
return this as unknown as Either<L, R2>;
}
getOrElse<R>(defaultValue: R): R {
return defaultValue;
}
match<T>(patterns: { left: (value: L) => T; right: (value: never) => T }): T {
return patterns.left(this.value);
}
}
class Right<R> {
readonly _tag = "Right" as const;
constructor(readonly value: R) {}
map<R2>(fn: (value: R) => R2): Either<never, R2> {
return new Right(fn(this.value));
}
flatMap<L, R2>(fn: (value: R) => Either<L, R2>): Either<L, R2> {
return fn(this.value);
}
getOrElse(_defaultValue: R): R {
return this.value;
}
match<T>(patterns: { left: (value: never) => T; right: (value: R) => T }): T {
return patterns.right(this.value);
}
}
const left = <L>(value: L): Either<L, never> => new Left(value);
const right = <R>(value: R): Either<never, R> => new Right(value);
// Usage: User registration pipeline
interface RegistrationError {
field: string;
message: string;
}
function validateEmail(email: string): Either<RegistrationError, string> {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email)
? right(email.toLowerCase())
: left({ field: "email", message: "Invalid email format" });
}
function validatePassword(password: string): Either<RegistrationError, string> {
if (password.length < 8) {
return left({ field: "password", message: "Password too short" });
}
return right(password);
}
function validateName(name: string): Either<RegistrationError, string> {
if (name.trim().length === 0) {
return left({ field: "name", message: "Name is required" });
}
return right(name.trim());
}
function registerUser(
email: string,
password: string,
name: string,
): Either<RegistrationError, { email: string; password: string; name: string }> {
return validateEmail(email).flatMap((validEmail) =>
validatePassword(password).flatMap((validPassword) =>
validateName(name).map((validName) => ({
email: validEmail,
password: validPassword,
name: validName,
})),
),
);
}
const result1 = registerUser("alice@example.com", "StrongPass1!", "Alice");
result1.match({
left: (err) => console.log(`Error in ${err.field}: ${err.message}`),
right: (user) => console.log(`Registered: ${user.email}`),
});
// Rust — Result is built into the language
#[derive(Debug)]
enum AppError {
ParseError(String),
ValidationError(String),
}
fn parse_age(input: &str) -> Result<u32, AppError> {
let age: u32 = input
.parse()
.map_err(|e| AppError::ParseError(format!("{e}")))?;
if age > 150 {
return Err(AppError::ValidationError(
"Age must be 150 or less".to_string()
));
}
Ok(age)
}
// Error propagation with ? operator
fn process_user_input(name: &str, age_str: &str) -> Result<String, AppError> {
let age = parse_age(age_str)?;
Ok(format!("{name} is {age} years old"))
}
10. IO Monad — Side Effect Isolation
10.1 The Problem with Side Effects
Real programs cannot be built with pure functions alone. Database reads, HTTP requests, file writes all require side effects. The IO monad separates describing side effects from executing them.
// TypeScript — IO Monad (simplified)
class IO<T> {
constructor(private readonly effect: () => T) {}
static of<T>(value: T): IO<T> {
return new IO(() => value);
}
static from<T>(effect: () => T): IO<T> {
return new IO(effect);
}
map<U>(fn: (value: T) => U): IO<U> {
return new IO(() => fn(this.effect()));
}
flatMap<U>(fn: (value: T) => IO<U>): IO<U> {
return new IO(() => fn(this.effect()).run());
}
run(): T {
return this.effect();
}
}
// Wrapping side effects in IO
const readEnv = (key: string): IO<string | undefined> =>
IO.from(() => process.env[key]);
const log = (message: string): IO<void> =>
IO.from(() => console.log(message));
const getCurrentTime = (): IO<Date> =>
IO.from(() => new Date());
// IO composition — nothing executed yet!
const program = readEnv("DATABASE_URL")
.flatMap((url) =>
url
? log(`Connecting to: ${url}`).map(() => url)
: IO.of("postgresql://localhost:5432/default"),
)
.flatMap((url) =>
getCurrentTime().flatMap((time) =>
log(`[${time.toISOString()}] Using: ${url}`).map(() => url),
),
);
// Execution happens here!
const dbUrl = program.run();
11. Functor and Applicative
11.1 Functor — Containers with map
A Functor is a container that provides a map operation. Maybe, Either, Array, and Promise are all Functors.
// Common trait of all Functors: transform inner value with map
[1, 2, 3].map((x) => x * 2); // [2, 4, 6]
Maybe.of(5).map((x) => x * 2); // Maybe(10)
Maybe.none().map((x) => x * 2); // Maybe(None)
Promise.resolve(5).then((x) => x * 2); // Promise(10)
// Functor Laws:
// 1. Identity: fa.map(x => x) === fa
// 2. Composition: fa.map(f).map(g) === fa.map(x => g(f(x)))
11.2 Applicative — Combining Multiple Functors
function liftA2<A, B, C>(
fn: (a: A, b: B) => C,
ma: Maybe<A>,
mb: Maybe<B>,
): Maybe<C> {
return ma.flatMap((a) => mb.map((b) => fn(a, b)));
}
const price = Maybe.of(100);
const quantity = Maybe.of(5);
const total = liftA2((p, q) => p * q, price, quantity);
console.log(total.getOrElse(0)); // 500
const noPrice = Maybe.none<number>();
const noResult = liftA2((p, q) => p * q, noPrice, quantity);
console.log(noResult.getOrElse(0)); // 0
12. Pattern Matching
12.1 TypeScript Discriminated Unions
type HttpResponse =
| { status: 200; data: unknown }
| { status: 404; path: string }
| { status: 500; error: Error }
| { status: 401; realm: string };
function handleResponse(response: HttpResponse): string {
switch (response.status) {
case 200: return `Success: ${JSON.stringify(response.data)}`;
case 404: return `Not found: ${response.path}`;
case 500: return `Server error: ${response.error.message}`;
case 401: return `Unauthorized: realm=${response.realm}`;
}
}
// Exhaustiveness checking
function assertNever(x: never): never {
throw new Error(`Unexpected value: ${x}`);
}
12.2 Rust Pattern Matching
enum Command {
Quit,
Echo(String),
Move { x: i32, y: i32 },
ChangeColor(u8, u8, u8),
}
fn process_command(cmd: Command) -> String {
match cmd {
Command::Quit => "Goodbye!".to_string(),
Command::Echo(msg) => msg,
Command::Move { x, y } => format!("Moving to ({x}, {y})"),
Command::ChangeColor(r, g, b) => format!("Color: rgb({r}, {g}, {b})"),
}
}
fn classify_number(n: i32) -> &'static str {
match n {
0 => "zero",
n if n > 0 => "positive",
_ => "negative",
}
}
13. Lenses — Immutable Nested Updates
Tools for elegantly updating deeply nested immutable data.
// TypeScript — Simple Lens implementation
interface Lens<S, A> {
get: (source: S) => A;
set: (value: A, source: S) => S;
}
function lens<S, A>(
get: (source: S) => A,
set: (value: A, source: S) => S,
): Lens<S, A> {
return { get, set };
}
function over<S, A>(l: Lens<S, A>, fn: (a: A) => A, source: S): S {
return l.set(fn(l.get(source)), source);
}
function composeLens<S, A, B>(outer: Lens<S, A>, inner: Lens<A, B>): Lens<S, B> {
return {
get: (source: S) => inner.get(outer.get(source)),
set: (value: B, source: S) => {
const outerValue = outer.get(source);
const newOuter = inner.set(value, outerValue);
return outer.set(newOuter, source);
},
};
}
// Usage
interface Company {
name: string;
ceo: { name: string; address: { city: string; country: string } };
}
const ceoLens = lens<Company, Company["ceo"]>(
(c) => c.ceo,
(ceo, c) => ({ ...c, ceo }),
);
const addressLens = lens<Company["ceo"], Company["ceo"]["address"]>(
(ceo) => ceo.address,
(address, ceo) => ({ ...ceo, address }),
);
const cityLens = lens<Company["ceo"]["address"], string>(
(addr) => addr.city,
(city, addr) => ({ ...addr, city }),
);
const ceoCityLens = composeLens(composeLens(ceoLens, addressLens), cityLens);
const company: Company = {
name: "TechCorp",
ceo: { name: "Alice", address: { city: "Seoul", country: "Korea" } },
};
const updated = ceoCityLens.set("Tokyo", company);
// company.ceo.address.city is still "Seoul"
// updated.ceo.address.city is "Tokyo"
14. FP Ecosystem by Language
14.1 TypeScript — fp-ts / Effect
// fp-ts style example (conceptual)
import { pipe } from "fp-ts/function";
import * as O from "fp-ts/Option";
import * as E from "fp-ts/Either";
const getUserName = (id: string): O.Option<string> =>
id === "1" ? O.some("Alice") : O.none;
const greeting = pipe(
getUserName("1"),
O.map((name) => `Hello, ${name}!`),
O.getOrElse(() => "Hello, Guest!"),
);
const parseNumber = (input: string): E.Either<string, number> => {
const n = Number(input);
return isNaN(n) ? E.left(`Invalid: ${input}`) : E.right(n);
};
const result = pipe(
parseNumber("42"),
E.map((n) => n * 2),
E.map((n) => `Result: ${n}`),
E.getOrElse((err) => `Error: ${err}`),
);
14.2 Python — returns library
from returns.result import Result, Success, Failure
from returns.maybe import Maybe, Some, Nothing
def safe_divide(a: float, b: float) -> Result[float, str]:
if b == 0:
return Failure("Division by zero")
return Success(a / b)
result = safe_divide(10, 3) # Success(3.333...)
result = safe_divide(10, 0) # Failure('Division by zero')
14.3 Rust — Built-in
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let result: Vec<i32> = numbers
.iter()
.filter(|&&x| x % 2 == 0)
.map(|&x| x * x)
.collect();
println!("{:?}", result); // [4, 16, 36, 64, 100]
let config = std::env::var("APP_PORT")
.ok()
.and_then(|s| s.parse::<u16>().ok())
.unwrap_or(8080);
let multiply_by = |factor: i32| {
move |x: i32| x * factor
};
let double = multiply_by(2);
println!("{}", double(5)); // 10
}
15. FP vs OOP — Pragmatic Comparison
Aspect FP OOP
─────────────────────────────────────────────────────────
Data and behavior Separated (data + functions) Combined (object = data + methods)
State management Immutable data transformation Internal state mutation
Polymorphism Pattern matching / typeclasses Inheritance / interfaces
Code reuse Function composition Inheritance / mixins
Error handling Either/Result returns Exception throwing
Concurrency Natural with immutability Requires locks/sync
Practical guide:
- Data transformation heavy: FP (pipelines, map/filter/reduce)
- Stateful entities heavy: OOP (encapsulation, polymorphism)
- Best approach: combine both paradigms
16. Real Backend Examples
16.1 Data Pipeline
// TypeScript — Functional data pipeline
interface RawEvent {
timestamp: string;
userId: string;
action: string;
metadata: Record<string, unknown>;
}
interface ProcessedEvent {
date: string;
userId: string;
action: string;
isValid: boolean;
}
const parseTimestamp = (event: RawEvent) => ({
...event,
date: new Date(event.timestamp).toISOString().split("T")[0],
});
const normalizeAction = (event: RawEvent & { date: string }) => ({
...event,
action: event.action.toLowerCase().trim(),
});
const validateEvent = (event: RawEvent & { date: string }) => ({
userId: event.userId,
date: event.date,
action: event.action,
isValid: Boolean(event.userId && event.action && event.date),
});
function processEvents(events: RawEvent[]): ProcessedEvent[] {
return events
.map(parseTimestamp)
.map(normalizeAction)
.map(validateEvent)
.filter((e) => e.isValid);
}
16.2 Validation Pipeline
# Python — Functional validation
from dataclasses import dataclass
from typing import Callable
@dataclass(frozen=True)
class ValidationError:
field: str
message: str
@dataclass(frozen=True)
class ValidationResult:
errors: tuple[ValidationError, ...]
is_valid: bool
@staticmethod
def success() -> "ValidationResult":
return ValidationResult(errors=(), is_valid=True)
@staticmethod
def failure(field: str, message: str) -> "ValidationResult":
return ValidationResult(
errors=(ValidationError(field, message),),
is_valid=False,
)
def combine(self, other: "ValidationResult") -> "ValidationResult":
if self.is_valid and other.is_valid:
return ValidationResult.success()
return ValidationResult(
errors=self.errors + other.errors,
is_valid=False,
)
Validator = Callable[[dict], ValidationResult]
def required(field: str) -> Validator:
def validate(data: dict) -> ValidationResult:
value = data.get(field)
if not value or (isinstance(value, str) and not value.strip()):
return ValidationResult.failure(field, f"{field} is required")
return ValidationResult.success()
return validate
def min_length(field: str, length: int) -> Validator:
def validate(data: dict) -> ValidationResult:
value = data.get(field, "")
if len(str(value)) < length:
return ValidationResult.failure(
field, f"{field} must be at least {length} characters"
)
return ValidationResult.success()
return validate
def email_format(field: str) -> Validator:
def validate(data: dict) -> ValidationResult:
import re
value = data.get(field, "")
pattern = r'^[^\s@]+@[^\s@]+\.[^\s@]+$'
if not re.match(pattern, str(value)):
return ValidationResult.failure(field, f"{field} must be a valid email")
return ValidationResult.success()
return validate
def validate_all(*validators: Validator) -> Validator:
def validate(data: dict) -> ValidationResult:
result = ValidationResult.success()
for validator in validators:
result = result.combine(validator(data))
return result
return validate
validate_user = validate_all(
required("name"),
required("email"),
email_format("email"),
required("password"),
min_length("password", 8),
)
data = {"name": "", "email": "invalid", "password": "short"}
result = validate_user(data)
for error in result.errors:
print(f"{error.field}: {error.message}")
17. Quiz
Q1. What are the two conditions of a pure function?
- Deterministic: Same input always returns the same output.
- No Side Effects: Does not modify external state and does not depend on external state (time, random, I/O, global variables, etc.).
When both conditions are met, referential transparency is guaranteed, making memoization, parallel execution, and lazy evaluation safe.
Q2. What is the difference between Maybe monad and Either monad?
Maybe (Option) represents only two cases: value present or absent. You cannot know why the value is absent.
Either (Result) represents success value or error value. The Left side carries error information to communicate the failure reason. Use Either when error handling is needed; use Maybe when simply checking value presence.
Q3. What is the difference between currying and partial application?
Currying: Transforms f(a, b, c) into f(a)(b)(c). Creates a chain of functions that each take exactly one argument.
Partial Application: Fixes some arguments of f(a, b, c) to create g(c) = f(fixed_a, fixed_b, c). Can fix multiple arguments at once.
Currying is a special form of partial application (one argument at a time).
Q4. What is structural sharing?
When updating immutable data structures, unchanged portions share the same memory references as the previous version. This minimizes memory usage and copy costs of immutable updates. Immer.js, Immutable.js, and Clojure's persistent data structures use this technique.
Q5. If you had to choose between FP and OOP?
You don't have to choose. The modern approach is combining both paradigms as appropriate.
- Data transformation, validation, business rules: FP style (pure functions, pipelines, Either)
- Stateful entities, external system interactions: OOP style (encapsulation, interfaces)
- Error handling: Either/Result (FP), can mix with try-catch
- Concurrency: prefer immutable data (FP)
TypeScript, Python, Kotlin, Scala, and Rust all support both paradigms.
18. References
- Hughes, J. — "Why Functional Programming Matters" (1989)
- Milewski, B. — Category Theory for Programmers — https://bartoszmilewski.com/
- fp-ts Documentation — https://gcanti.github.io/fp-ts/
- Effect Documentation — https://effect.website/
- Python returns — https://returns.readthedocs.io/
- Rust Book — Functional Features — https://doc.rust-lang.org/book/ch13-00-functional-features.html
- Wlaschin, S. — Domain Modeling Made Functional
- Frisby, B. — Professor Frisby's Mostly Adequate Guide to FP — https://mostly-adequate.gitbook.io/
- Haskell Wiki — Monad — https://wiki.haskell.org/Monad
- Martin, R.C. — Functional Design: Principles, Patterns, and Practices
- TypeScript Handbook — Discriminated Unions — https://www.typescriptlang.org/docs/handbook/2/narrowing.html
- Immer.js — https://immerjs.github.io/immer/
- toolz — https://toolz.readthedocs.io/
현재 단락 (1/966)
Functional programming (FP) is not an academic curiosity but a **practical weapon** in modern backen...