目次
1. なぜバックエンド開発者(かいはつしゃ)に関数型(かんすうがた)プログラミングが必要(ひつよう)か
関数型プログラミング(FP)は学術的(がくじゅつてき)な好奇心(こうきしん)ではなく、現代(げんだい)のバックエンド開発における**実戦(じっせん)の武器(ぶき)**である。
バックエンドでFPが輝(かがや)く理由(りゆう):
- テスト容易性(よういせい) — 純粋関数は入力だけで出力を検証(けんしょう)できる。モッキング不要(ふよう)
- 並行性(へいこうせい)安全 — 不変データはロックなしで複数(ふくすう)スレッドで安全に共有(きょうゆう)できる
- 保守性(ほしゅせい) — 小さな関数の合成はコードを読みやすく変更(へんこう)しやすくする
- エラー処理(しょり) — Either/Resultモナドは例外(れいがい)なしで安全なエラー処理を可能(かのう)にする
- データパイプライン — map/filter/reduceでデータ変換(へんかん)を宣言的(せんげんてき)に表現(ひょうげん)する
FPはOOPを置(お)き換(か)えるものではない。両方(りょうほう)のパラダイムを状況(じょうきょう)に応(おう)じて組み合わせるのが現代的(げんだいてき)なアプローチである。
関数型プログラミング コア概念
基礎
├── 純粋関数 (Pure Function)
├── 不変性 (Immutability)
├── 第一級関数 (First-class Function)
└── 高階関数 (Higher-order Function)
合成
├── 関数合成 (Function Composition)
├── カリー化 (Currying)
├── 部分適用 (Partial Application)
└── ポイントフリースタイル (Point-free)
型とパターン
├── 代数的データ型 (ADT)
├── パターンマッチング (Pattern Matching)
├── レンズ (Lens)
└── オプティック (Optic)
モナドとコンテナ
├── Functor (map)
├── Applicative (ap)
├── Monad (flatMap/bind)
├── Maybe/Option (null安全)
├── Either/Result (エラー処理)
└── IO (副作用隔離)
2. 純粋関数 (Pure Functions)
2.1 定義(ていぎ)
純粋関数は2つの条件(じょうけん)を満(み)たす:
- 同(おな)じ入力(にゅうりょく)に常(つね)に同じ出力(しゅつりょく) (決定的(けっていてき), deterministic)
- 副作用(ふくさよう)がない (外部(がいぶ)状態(じょうたい)を変更(へんこう)しない)
// 純粋関数
function add(a: number, b: number): number {
return a + b; // 常に同じ結果、副作用なし
}
function formatPrice(amount: number, currency: string): string {
return `${currency} ${amount.toFixed(2)}`;
}
// 非純粋関数 — 外部状態に依存
let taxRate = 0.1;
function calculateTax(amount: number): number {
return amount * taxRate; // taxRateが変われば結果が変わる
}
// 非純粋関数 — 副作用あり
function saveUser(user: User): void {
database.save(user); // 外部状態変更
console.log("User saved"); // I/O副作用
}
2.2 参照透過性(さんしょうとうかせい) (Referential Transparency)
純粋関数の呼(よ)び出(だ)しをその結果値(けっかち)で置き換(か)えてもプログラムの意味(いみ)が変(か)わらない。これが参照透過性である。
// 参照透過的
const x = add(3, 4); // 7
const y = add(3, 4); // 7
// add(3, 4)をすべて7に置き換えてもプログラムは同じ
// 参照不透過
const now1 = Date.now(); // 1713000000000
const now2 = Date.now(); // 1713000000001
// Date.now()を特定の値に置き換えられない
2.3 メモ化(か) (Memoization)
純粋関数は同じ入力に同じ出力を保証(ほしょう)するので、結果をキャッシュできる。
// 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)); // 初回: 遅い
console.log(memoFib(40)); // 2回目: 即座(キャッシュ)
# 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 なぜ不変性が重要(じゅうよう)か
可変(かへん)状態はバグの主犯(しゅはん)である。データがいつ、どこで、誰(だれ)によって変更されるか追跡(ついせき)しにくい。
// 可変 — 危険
const users = [{ name: "Alice", age: 30 }, { name: "Bob", age: 25 }];
function birthday(user: { name: string; age: number }) {
user.age += 1; // 原本を直接変更!
return user;
}
birthday(users[0]);
// users[0].ageが31に — 予期しない副作用
// 不変 — 安全
function birthdayImmutable(user: { name: string; age: number }) {
return { ...user, age: user.age + 1 }; // 新しいオブジェクト生成
}
const olderAlice = birthdayImmutable(users[0]);
// users[0].ageはまだ30 — 原本保存
3.2 TypeScriptでの不変性
// Readonlyユーティリティ型
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; // コンパイルエラー!
// config.features.push("cache"); // コンパイルエラー!
// DeepReadonly — 深い不変性
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
// 不変更新パターン
interface State {
user: { name: string; preferences: { theme: string; lang: string } };
}
function updateTheme(state: State, newTheme: string): State {
return {
...state,
user: {
...state.user,
preferences: {
...state.user.preferences,
theme: newTheme,
},
},
};
}
3.3 構造的(こうぞうてき)共有(きょうゆう) (Structural Sharing)
不変データは毎回(まいかい)全体(ぜんたい)をコピーするわけではない。変更されていない部分は参照(さんしょう)を共有する。
// Immer.js — 不変更新を可変のように簡単に
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.version += 1;
});
// initialStateは変更されていない
// newState.users[1] === initialState.users[1] // true (構造的共有)
3.4 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 (原本保存)
print(older_alice.age) # 31 (新オブジェクト)
# NamedTuple — 不変タプル
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)
3.5 Rustでの不変性
// Rust — デフォルトが不変
fn main() {
let x = 5;
// x = 6; // コンパイルエラー!変数はデフォルトで不変
let mut y = 5; // mutキーワードで明示的に可変
y = 6; // OK
let data = vec![1, 2, 3];
let sum = calculate_sum(&data); // 不変参照
println!("Sum: {sum}");
}
fn calculate_sum(numbers: &[i32]) -> i32 {
numbers.iter().sum()
}
4. 高階(こうかい)関数 (Higher-Order Functions)
高階関数は関数を引数(ひきすう)として受(う)け取(と)る、または関数を返(かえ)す関数である。
4.1 map / filter / reduce
// TypeScript — データ変換パイプライン
interface Transaction {
id: string;
amount: number;
type: "credit" | "debit";
category: string;
}
const transactions: Transaction[] = [
{ id: "1", amount: 100, type: "credit", category: "salary" },
{ id: "2", amount: 50, type: "debit", category: "food" },
{ id: "3", amount: 200, type: "credit", category: "freelance" },
{ id: "4", amount: 30, type: "debit", category: "transport" },
{ id: "5", amount: 80, type: "debit", category: "food" },
];
// 宣言的データ処理
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
// カテゴリ別集計
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;
}, {});
4.2 関数を返す関数
// バリデーション関数ファクトリ
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;
};
}
const validatePassword = composeValidators(
minLength(8),
maxLength(128),
matches(/[A-Z]/, "Must contain uppercase letter"),
matches(/[0-9]/, "Must contain a number"),
);
# Python — 高階関数
from typing import Callable
def pipe(*funcs: Callable) -> Callable:
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)
小さな関数を連結(れんけつ)して複雑(ふくざつ)なロジックを構成(こうせい)することが関数型プログラミングの核心(かくしん)である。
// TypeScript — pipeユーティリティ
function pipe(initial: unknown, ...fns: Function[]): unknown {
return fns.reduce((acc, fn) => fn(acc), initial);
}
// データ変換パイプライン
interface RawUser {
first_name: string;
last_name: string;
email_address: string;
birth_date: string;
}
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 }) => ({
...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);
6. カリー化(か)と部分適用(ぶぶんてきよう)
6.1 カリー化 (Currying)
複数(ふくすう)の引数を受け取る関数を、一度(いちど)に一つの引数だけ受け取る関数チェーンに変換(へんかん)する。
// TypeScript カリー化
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
// 実践: APIリクエストビルダー
const request = (method: string) => (baseUrl: string) => (path: string) =>
fetch(`${baseUrl}${path}`, { method });
const get = request("GET");
const apiGet = get("https://api.example.com");
await apiGet("/users");
await apiGet("/products");
6.2 部分適用 (Partial Application)
関数の一部の引数を事前(じぜん)に固定(こてい)して新(あたら)しい関数を作る。
# 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
7. 代数的(だいすうてき)データ型(がた) (Algebraic Data Types)
7.1 直和型(ちょくわがた) (Sum Types) — ユニオン/列挙(れっきょ)型
複数の可能な形態(けいたい)のうち一つを持つ型。
// 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;
}
}
# Python — match (3.10+)
from dataclasses import dataclass
@dataclass
class Circle:
radius: float
@dataclass
class Rectangle:
width: float
height: float
Shape = Circle | Rectangle
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
// Rust — enum (真の代数的データ型)
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モナド — null安全(あんぜん)
8.1 nullの問題
null/undefinedチェックを忘(わす)れるとランタイムエラーが発生する。Tony HoareはnullをS「10億(おく)ドルの失敗(しっぱい)」と呼(よ)んだ。
8.2 Maybe/Optionモナド
// TypeScript — Maybe Monad実装
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!;
}
}
// 使用 — きれいなnull安全チェイニング
interface User {
name: string;
address?: { city?: string };
}
function findUser(id: string): Maybe<User> {
const users: Record<string, User> = {
"1": { name: "Alice", address: { city: "Tokyo" } },
"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); // "Tokyo"
const unknownCity = findUser("999")
.flatMap((user) => Maybe.of(user.address))
.map((address) => address.city)
.getOrElse("Unknown");
console.log(unknownCity); // "Unknown" — エラーなし!
// Rust — Optionは言語に内蔵
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())
}
// ?演算子でより簡潔に
fn get_user_city_short(id: &str) -> Option<String> {
let user = find_user(id)?;
let address = user.address?;
Some(address.city)
}
9. Either/Resultモナド — 例外(れいがい)なしのエラー処理
9.1 例外の問題
例外はコードフローを予測(よそく)不可能(ふかのう)にする。どの関数がどの例外を投(な)げるか型システムが保証(ほしょう)しない。
9.2 Either/Result実装
// 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;
}
}
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;
}
}
const left = <L>(value: L): Either<L, never> => new Left(value);
const right = <R>(value: R): Either<never, R> => new Right(value);
// 使用: ユーザー登録パイプライン
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);
}
const result = validateEmail("alice@example.com")
.flatMap((email) =>
validatePassword("StrongPass1!").map((password) => ({
email,
password,
})),
);
// Rust — Resultは言語に内蔵
#[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)
}
// ?演算子でエラー伝播
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モナド — 副作用隔離(かくり)
純粋関数だけでは実際(じっさい)のプログラムを作れない。IOモナドは副作用を**記述(きじゅつ)することと実行(じっこう)**することを分離する。
// TypeScript — IO Monad
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();
}
}
const readEnv = (key: string): IO<string | undefined> =>
IO.from(() => process.env[key]);
const log = (message: string): IO<void> =>
IO.from(() => console.log(message));
// IO合成 — まだ実行されていない!
const program = readEnv("DATABASE_URL")
.flatMap((url) =>
url
? log(`Connecting to: ${url}`).map(() => url)
: IO.of("postgresql://localhost:5432/default"),
);
// ここで初めて実行!
const dbUrl = program.run();
11. FunctorとApplicative
11.1 Functor — mapを持つコンテナ
Functorはmap演算を提供(ていきょう)するコンテナである。Maybe、Either、Array、Promiseはすべてfunctorである。
// すべてのFunctorの共通点: mapで内部値を変換
[1, 2, 3].map((x) => x * 2); // [2, 4, 6]
Maybe.of(5).map((x) => x * 2); // Maybe(10)
Promise.resolve(5).then((x) => x * 2); // Promise(10)
// Functor法則:
// 1. Identity: fa.map(x => x) === fa
// 2. Composition: fa.map(f).map(g) === fa.map(x => g(f(x)))
11.2 Applicative — 複数Functorの結合(けつごう)
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
12. パターンマッチング (Pattern Matching)
// TypeScript — Discriminated Union
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 `Error: ${response.error.message}`;
case 401: return `Unauthorized: realm=${response.realm}`;
}
}
// 完全性検査 — exhaustive check
function assertNever(x: never): never {
throw new Error(`Unexpected: ${x}`);
}
// Rust — 強力なパターンマッチング
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) — 不変(ふへん)ネスト更新(こうしん)
深くネストされた不変データを優雅(ゆうが)に更新するためのツール。
// TypeScript — 簡単なLens実装
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 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);
},
};
}
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: "Tokyo", country: "Japan" } },
};
const updated = ceoCityLens.set("Osaka", company);
// company.ceo.address.cityはまだ"Tokyo"
// updated.ceo.address.cityは"Osaka"
14. 言語別(げんごべつ)FPエコシステム
14.1 TypeScript — fp-ts / Effect
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!"),
);
14.2 Python — returns
from returns.result import Result, Success, Failure
def safe_divide(a: float, b: float) -> Result[float, str]:
if b == 0:
return Failure("Division by zero")
return Success(a / b)
14.3 Rust — 基本内蔵(きほんないぞう)
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 — 実用的(じつようてき)比較(ひかく)
観点 FP OOP
────────────────────────────────────────────────────────
データと振る舞い 分離 (データ + 関数) 結合 (オブジェクト = データ + メソッド)
状態管理 不変データ変換 内部状態変更
多態性 パターンマッチ/型クラス 継承/インターフェース
コード再利用 関数合成 継承/ミックスイン
エラー処理 Either/Result返却 例外スロー
並行性 不変性で自然 ロック/同期が必要
実践ガイド:
- データ変換中心: FP (パイプライン、map/filter/reduce)
- 状態を持つエンティティ中心: OOP (カプセル化、多態性)
- 最良(さいりょう)のアプローチ: 両方のパラダイムの長所(ちょうしょ)を組み合わせる
16. 実戦バックエンド例
16.1 データパイプライン
interface RawEvent {
timestamp: string;
userId: string;
action: string;
}
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 バリデーションパイプライン
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 validate_all(*validators: Validator) -> Validator:
def validate(data: dict) -> ValidationResult:
result = ValidationResult.success()
for v in validators:
result = result.combine(v(data))
return result
return validate
validate_user = validate_all(
required("name"),
required("email"),
required("password"),
min_length("password", 8),
)
data = {"name": "", "email": "", "password": "short"}
result = validate_user(data)
for err in result.errors:
print(f"{err.field}: {err.message}")
17. クイズ
Q1. 純粋関数の2つの条件は?
- 決定的(けっていてき)(Deterministic): 同じ入力に常に同じ出力を返す。
- 副作用なし(No Side Effects): 外部状態を変更せず、外部状態に依存しない(時間、乱数(らんすう)、I/O、グローバル変数など)。
この2つの条件を満たすと参照透過性が保証され、メモ化、並列(へいれつ)実行、遅延(ちえん)評価(ひょうか)が安全に可能になる。
Q2. MaybeモナドとEitherモナドの違いは?
Maybe(Option)は値があるかないかの2つのケースのみ表現する。値がない理由はわからない。
Either(Result)は成功値またはエラー値を表現する。Left側にエラー情報を入れて失敗原因を伝達できる。エラー処理が必要な場合はEither、単純に値の有無を確認する時はMaybeを使用する。
Q3. カリー化と部分適用の違いは?
カリー化: f(a, b, c)をf(a)(b)(c)の形に変換。常に一つずつ引数を受け取る関数チェーンになる。
部分適用: f(a, b, c)の一部の引数を固定してg(c) = f(fixed_a, fixed_b, c)を作る。一度に複数の引数を固定できる。
カリー化は部分適用の特殊(とくしゅ)な形(一つずつ)である。
Q4. 構造的共有(Structural Sharing)とは?
不変データ構造を更新する際、変更されていない部分は以前(いぜん)のバージョンと同じメモリを参照(共有)する手法。これにより不変更新のメモリ使用量とコピーコストを最小化(さいしょうか)する。Immer.js、Immutable.js、Clojureの永続(えいぞく)データ構造などがこの方法を使用する。
Q5. FPとOOPのどちらを選ぶべきか?
選(えら)ぶ必要はない。現代的なアプローチは両方のパラダイムを状況に応じて組み合わせることである。
- データ変換、バリデーション、ビジネスルール: FPスタイル(純粋関数、パイプライン、Either)
- 状態を持つエンティティ、外部システムとの対話: OOPスタイル(カプセル化、インターフェース)
- エラー処理: Either/Result (FP)、try-catchと混合可能
- 並行性: 不変データ (FP) を優先(ゆうせん)
TypeScript、Python、Kotlin、Scala、Rustはすべて両方のパラダイムをサポートする。
18. 参考資料(さんこうしりょう)
- 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/822)
関数型プログラミング(FP)は学術的(がくじゅつてき)な好奇心(こうきしん)ではなく、現代(げんだい)のバックエンド開発における**実戦(じっせん)の武器(ぶき)**である。