Skip to content

✍️ 필사 모드: バックエンド開発者のための関数型プログラミングガイド 2025: 不変性、モナド、合成、副作用管理

日本語
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

目次

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つの条件(じょうけん)を満(み)たす:

  1. 同(おな)じ入力(にゅうりょく)に常(つね)に同じ出力(しゅつりょく) (決定的(けっていてき), deterministic)
  2. 副作用(ふくさよう)がない (外部(がいぶ)状態(じょうたい)を変更(へんこう)しない)
// 純粋関数
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つの条件は?
  1. 決定的(けっていてき)(Deterministic): 同じ入力に常に同じ出力を返す。
  2. 副作用なし(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. 参考資料(さんこうしりょう)

  1. Hughes, J. — "Why Functional Programming Matters" (1989)
  2. Milewski, B. — Category Theory for Programmers — https://bartoszmilewski.com/
  3. fp-ts Documentation — https://gcanti.github.io/fp-ts/
  4. Effect Documentation — https://effect.website/
  5. Python returns — https://returns.readthedocs.io/
  6. Rust Book — Functional Features — https://doc.rust-lang.org/book/ch13-00-functional-features.html
  7. Wlaschin, S. — Domain Modeling Made Functional
  8. Frisby, B. — Professor Frisby's Mostly Adequate Guide to FP — https://mostly-adequate.gitbook.io/
  9. Haskell Wiki — Monad — https://wiki.haskell.org/Monad
  10. Martin, R.C. — Functional Design: Principles, Patterns, and Practices
  11. TypeScript Handbook — Discriminated Unions — https://www.typescriptlang.org/docs/handbook/2/narrowing.html
  12. Immer.js — https://immerjs.github.io/immer/
  13. toolz — https://toolz.readthedocs.io/

현재 단락 (1/822)

関数型プログラミング(FP)は学術的(がくじゅつてき)な好奇心(こうきしん)ではなく、現代(げんだい)のバックエンド開発における**実戦(じっせん)の武器(ぶき)**である。

작성 글자: 0원문 글자: 23,318작성 단락: 0/822