Skip to content
Published on

関数型プログラミング実践ガイド:バックエンド開発者のための不変性・純粋関数・モナドの全て

Authors

はじめに:なぜ2025年(ねん)に関数型(かんすうがた)プログラミングなのか

2025年(ねん)、関数型(かんすうがた)プログラミング(FP)はもはや学術的(がくじゅつてき)な好奇心(こうきしん)の領域(りょういき)ではありません。React Hooksは関数型(かんすうがた)コンポーネントを標準(ひょうじゅん)にし、Rustの所有権(しょゆうけん)システムは不変性(ふへんせい)を言語(げんご)レベルで強制(きょうせい)します。TypeScriptエコシステムではfp-tsとEffectが急速(きゅうそく)に成長(せいちょう)しており、Pythonもfunctoolsとパターンマッチングを継続的(けいぞくてき)に強化(きょうか)しています。

なぜバックエンド開発者(かいはつしゃ)にFPが重要(じゅうよう)なのか:

  • 並行性(へいこうせい)の安全性(あんぜんせい): 不変(ふへん)データはrace conditionを根本的(こんぽんてき)に排除(はいじょ)します
  • テスト容易性(よういせい): 純粋関数(じゅんすいかんすう)はモックなしでテストできます
  • 合成可能性(かのうせい): 小(ちい)さな関数(かんすう)を組(く)み合(あ)わせて複雑(ふくざつ)なビジネスロジックを構築(こうちく)します
  • エラー処理(しょり): Option/Result型(かた)でnullと例外(れいがい)を体系的(たいけいてき)に管理(かんり)します
┌──────────────────────────────────────────────────┐
FPがメインストリームになった理由 (2025)├──────────────────────────────────────────────────┤
React Hooks    → 関数型コンポーネント標準化      │
Rust           → 所有権 + 不変性の強制           │
TypeScript     → fp-ts, Effect エコシステム成長  │
Java 21+Record, Pattern MatchingPython 3.12+   → match文, functools強化         │
Kotlin         → コルーチン + 関数型組込み       │
└──────────────────────────────────────────────────┘

この記事(きじ)では、FPの核心概念(かくしんがいねん)をTypeScript、Python、Scalaコードと共(とも)に説明(せつめい)し、バックエンド実践(じっせん)パターンまでカバーします。


1. 不変性(ふへんせい)(Immutability)

1.1 不変性(ふへんせい)とは

不変性(ふへんせい)は、データを一度(いちど)生成(せいせい)した後(あと)、絶対(ぜったい)に変更(へんこう)しない原則(げんそく)です。変更(へんこう)が必要(ひつよう)な場合(ばあい)は、既存(きぞん)データをコピーして新(あたら)しいデータを作成(さくせい)します。

// 可変(Mutable)- 危険
const user = { name: "Alice", age: 30 };
user.age = 31; // 元データ変更 → 他の参照で予期しない動作

// 不変(Immutable)- 安全
const user = { name: "Alice", age: 30 };
const updatedUser = { ...user, age: 31 }; // 新しいオブジェクト生成
// userは依然として { name: "Alice", age: 30 }

1.2 なぜ不変性(ふへんせい)が重要(じゅうよう)なのか

┌──────────────────────────────────────┐
Mutable Stateの問題├──────────────────────────────────────┤
Thread A: user.balance = 100Thread B: user.balance = 200│  結果: ??? (Race Condition)├──────────────────────────────────────┤
Immutable Stateの解決├──────────────────────────────────────┤
Thread A: newUser = {...user, 100}Thread B: newUser = {...user, 200}│  結果: 各自独立 (安全)└──────────────────────────────────────┘

1.3 言語(げんご)別(べつ)不変性(ふへんせい)サポート

TypeScript:

// readonly キーワード
interface User {
  readonly name: string;
  readonly age: number;
}

// as const
const CONFIG = {
  port: 3000,
  host: "localhost",
} as const;

// Readonly ユーティリティ型
type ImmutableUser = Readonly<User>;

// 深い不変性 (DeepReadonly)
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};

Python:

from dataclasses import dataclass
from typing import NamedTuple

# frozen dataclass
@dataclass(frozen=True)
class User:
    name: str
    age: int

user = User(name="Alice", age=30)
# user.age = 31  # FrozenInstanceError!

# 更新: replace使用
from dataclasses import replace
updated = replace(user, age=31)

# NamedTupleも不変
class Point(NamedTuple):
    x: float
    y: float

Scala:

// case classはデフォルトで不変
case class User(name: String, age: Int)

val user = User("Alice", 30)
val updated = user.copy(age = 31)

// 不変コレクションがデフォルト
val list = List(1, 2, 3)
val newList = 0 :: list  // List(0, 1, 2, 3) — 元のリスト保持

1.4 構造的共有(こうぞうてききょうゆう)(Structural Sharing)

不変(ふへん)データが毎回(まいかい)全体(ぜんたい)コピーをすればパフォーマンス問題(もんだい)になります。構造的共有(こうぞうてききょうゆう)は変更(へんこう)されていない部分(ぶぶん)を共有(きょうゆう)してこれを解決(かいけつ)します。

元のツリー:          更新後のツリー:
    A                     A'
   / \                   / \
  B   C                 B   C'
 / \   \               / \   \
D   E   F             D   E   F'

B, D, Eは共有(コピー不要)
AA', C→C', FF'のみ新規作成

JavaScriptではImmer、Immutable.jsがこれをサポートします。

import { produce } from "immer";

const state = {
  users: [
    { name: "Alice", score: 100 },
    { name: "Bob", score: 200 },
  ],
};

const newState = produce(state, (draft) => {
  draft.users[0].score = 150;
});

// state.users[1] === newState.users[1]  → true (構造的共有)

2. 純粋関数(じゅんすいかんすう)(Pure Functions)

2.1 純粋関数(じゅんすいかんすう)の定義(ていぎ)

純粋関数(じゅんすいかんすう)は2つの条件(じょうけん)を満(み)たします:

  1. 決定論的(けっていろんてき): 同(おな)じ入力(にゅうりょく)に常(つね)に同(おな)じ出力(しゅつりょく)
  2. 副作用(ふくさよう)なし: 外部(がいぶ)状態(じょうたい)を変更(へんこう)しない
// 純粋関数
function add(a: number, b: number): number {
  return a + b;
}

// 不純な関数 - 外部状態に依存
let counter = 0;
function increment(): number {
  return ++counter; // 副作用: 外部変数を変更
}

// 不純な関数 - 非決定論的
function now(): number {
  return Date.now(); // 毎回異なる結果
}

2.2 参照透過性(さんしょうとうかせい)(Referential Transparency)

式(しき)をその結果値(けっかち)に置(お)き換(か)えてもプログラムの動作(どうさ)が変(か)わらない性質(せいしつ)です。

// 参照透過
const x = add(2, 3); // 5
// add(2, 3)をどこでも5に置き換え可能

// 参照不透過
const y = Math.random();
// Math.random()をyの値に置き換えると動作が変わる

2.3 副作用(ふくさよう)の分離戦略(せんりゃく)

実践(じっせん)では副作用(ふくさよう)を完全(かんぜん)に避(さ)けることはできません。核心(かくしん)は純粋(じゅんすい)な部分(ぶぶん)と不純(ふじゅん)な部分(ぶぶん)を分離(ぶんり)することです。

// 悪い例: ビジネスロジックに副作用が混在
async function processOrder(orderId: string) {
  const order = await db.findOrder(orderId);         // 副作用
  const discount = calculateDiscount(order);          // 純粋
  const total = applyDiscount(order.amount, discount); // 純粋
  await db.updateOrder(orderId, total);               // 副作用
  await emailService.send(order.email, total);        // 副作用
}

// 良い例: 純粋ロジックを分離
// 純粋関数
function calculateDiscount(order: Order): number {
  if (order.items.length > 10) return 0.1;
  if (order.totalAmount > 100000) return 0.05;
  return 0;
}

function applyDiscount(amount: number, rate: number): number {
  return amount * (1 - rate);
}

function computeOrderResult(order: Order): OrderResult {
  const discount = calculateDiscount(order);
  const total = applyDiscount(order.amount, discount);
  return { orderId: order.id, total, discount };
}

// 不純なシェル(薄く保つ)
async function processOrder(orderId: string) {
  const order = await db.findOrder(orderId);
  const result = computeOrderResult(order);  // 純粋ロジック呼び出し
  await db.updateOrder(result.orderId, result.total);
  await emailService.send(order.email, result.total);
}
┌──────────────────────────────────────────────┐
Functional Core / Imperative Shell├──────────────────────────────────────────────┤
│                                              │
│  ┌────────────────────────────────┐          │
│  │     Imperative Shell           │          │
  (DB, Network, File I/O)       │          │
│  │  ┌────────────────────────┐    │          │
│  │  │   Functional Core      │    │          │
│  │    (純粋ビジネスロジック)  │    │          │
│  │  │  - 計算                 │    │          │
│  │  │  - 変換                 │    │          │
│  │  │  - 検証                 │    │          │
│  │  └────────────────────────┘    │          │
│  └────────────────────────────────┘          │
│                                              │
└──────────────────────────────────────────────┘

3. 高階関数(こうかいかんすう)(Higher-Order Functions)

3.1 高階関数(こうかいかんすう)とは

関数(かんすう)を引数(ひきすう)として受(う)け取(と)るか、関数(かんすう)を返(かえ)す関数(かんすう)です。

// 関数を引数として受け取る
function map<T, U>(arr: T[], fn: (item: T) => U): U[] {
  const result: U[] = [];
  for (const item of arr) {
    result.push(fn(item));
  }
  return result;
}

// 関数を返す
function multiplier(factor: number): (n: number) => number {
  return (n: number) => n * factor;
}

const double = multiplier(2);
const triple = multiplier(3);
console.log(double(5));  // 10
console.log(triple(5));  // 15

3.2 map, filter, reduce

const orders = [
  { id: 1, amount: 50000, status: "completed" },
  { id: 2, amount: 30000, status: "pending" },
  { id: 3, amount: 80000, status: "completed" },
  { id: 4, amount: 20000, status: "cancelled" },
];

// 命令型アプローチ
let total = 0;
for (const order of orders) {
  if (order.status === "completed") {
    total += order.amount;
  }
}

// 関数型アプローチ
const totalFP = orders
  .filter((o) => o.status === "completed")
  .map((o) => o.amount)
  .reduce((sum, amt) => sum + amt, 0);
// 130000

3.3 関数合成(かんすうごうせい)(Function Composition)

小(ちい)さな関数(かんすう)を組(く)み合(あ)わせて複雑(ふくざつ)な変換(へんかん)を作(つく)ります。

// 手動合成
const compose = <A, B, C>(
  f: (b: B) => C,
  g: (a: A) => B
): ((a: A) => C) => (a: A) => f(g(a));

const pipe = <A, B, C>(
  f: (a: A) => B,
  g: (b: B) => C
): ((a: A) => C) => (a: A) => g(f(a));

// 使用例
const trim = (s: string) => s.trim();
const toLowerCase = (s: string) => s.toLowerCase();
const split = (sep: string) => (s: string) => s.split(sep);

const tokenize = pipe(
  trim,
  pipe(toLowerCase, split(" "))
);

tokenize("  Hello World  "); // ["hello", "world"]

4. クロージャとカリー化(か)

4.1 クロージャ(Closure)

クロージャは関数(かんすう)が宣言(せんげん)されたレキシカル環境(かんきょう)を記憶(きおく)することです。

function createCounter(initial: number = 0) {
  let count = initial;
  return {
    increment: () => ++count,
    decrement: () => --count,
    getCount: () => count,
  };
}

const counter = createCounter(10);
counter.increment(); // 11
counter.increment(); // 12
counter.getCount();  // 12

実践活用(じっせんかつよう):設定(せってい)ファクトリ

function createLogger(prefix: string, level: string) {
  return (message: string) => {
    console.log(`[${prefix}] [${level}] ${message}`);
  };
}

const infoLogger = createLogger("MyApp", "INFO");
const errorLogger = createLogger("MyApp", "ERROR");

infoLogger("Server started");        // [MyApp] [INFO] Server started
errorLogger("DB connection failed"); // [MyApp] [ERROR] DB connection failed

4.2 カリー化(か)(Currying)

多引数関数(たひきすうかんすう)を単引数関数(たんひきすうかんすう)のチェーンに変換(へんかん)する技法(ぎほう)です。

// 通常の関数
function add(a: number, b: number): number {
  return a + b;
}

// カリー化された関数
function curriedAdd(a: number): (b: number) => number {
  return (b: number) => a + b;
}

const add5 = curriedAdd(5);
add5(3);  // 8
add5(10); // 15

// ジェネリックカリーユーティリティ
function curry<A, B, C>(fn: (a: A, b: B) => C): (a: A) => (b: B) => C {
  return (a: A) => (b: B) => fn(a, b);
}

4.3 部分適用(ぶぶんてきよう)(Partial Application)

// 実践例: APIクライアントファクトリ
function createApiClient(baseUrl: string, headers: Record<string, string>) {
  return async function request(method: string, path: string, body?: unknown) {
    const response = await fetch(`${baseUrl}${path}`, {
      method,
      headers,
      body: body ? JSON.stringify(body) : undefined,
    });
    return response.json();
  };
}

const api = createApiClient("https://api.example.com", {
  Authorization: "Bearer token123",
  "Content-Type": "application/json",
});

// 以降シンプルに使用
await api("GET", "/users");
await api("POST", "/orders", { item: "book", qty: 1 });

5. 代数的(だいすうてき)データ型(がた)(Algebraic Data Types)

5.1 和型(わがた)(Sum Types / Union Types)

複数(ふくすう)の可能(かのう)な形(かたち)のうち一(ひと)つを持(も)つ型(がた)です。「AまたはBまたはC」です。

// 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 (3.10+ match):

from dataclasses import dataclass
from math import pi

@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 pi * r ** 2
        case Rectangle(width=w, height=h):
            return w * h
        case Triangle(base=b, height=h):
            return b * h / 2

Scala:

enum Shape:
  case Circle(radius: Double)
  case Rectangle(width: Double, height: Double)
  case Triangle(base: Double, height: Double)

def area(shape: Shape): Double = shape match
  case Shape.Circle(r)         => Math.PI * r * r
  case Shape.Rectangle(w, h)   => w * h
  case Shape.Triangle(b, h)    => b * h / 2

5.2 ADTを活用(かつよう)した状態(じょうたい)モデリング

// 悪い例: booleanフラグの乱用
interface RequestState {
  isLoading: boolean;
  isError: boolean;
  data: Data | null;
  error: Error | null;
}
// isLoading=true, isError=true → 不可能な状態が表現可能

// 良い例: ADTで不可能な状態を排除
type RequestState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; error: Error };

function renderState<T>(state: RequestState<T>): string {
  switch (state.status) {
    case "idle":    return "待機中";
    case "loading": return "読み込み中...";
    case "success": return `データ: ${state.data}`;
    case "error":   return `エラー: ${state.error.message}`;
  }
}

6. Option/MaybeとResult/Either

6.1 Nullの問題(もんだい):10億(おく)ドルの間違(まちが)い

Tony HoareはNull参照(さんしょう)を「10億(おく)ドルの間違(まちが)い」と呼(よ)びました。Option型(がた)は値(あたい)が存在(そんざい)するかもしれないし、しないかもしれないことを型(がた)で表現(ひょうげん)します。

// Option型の実装
type Option<T> = { tag: "some"; value: T } | { tag: "none" };

const some = <T>(value: T): Option<T> => ({ tag: "some", value });
const none: Option<never> = { tag: "none" };

function map<T, U>(opt: Option<T>, fn: (v: T) => U): Option<U> {
  return opt.tag === "some" ? some(fn(opt.value)) : none;
}

function flatMap<T, U>(opt: Option<T>, fn: (v: T) => Option<U>): Option<U> {
  return opt.tag === "some" ? fn(opt.value) : none;
}

function getOrElse<T>(opt: Option<T>, defaultValue: T): T {
  return opt.tag === "some" ? opt.value : defaultValue;
}

6.2 Result/Either: 例外(れいがい)なしのエラー処理(しょり)

// Result型の実装
type Result<T, E> =
  | { tag: "ok"; value: T }
  | { tag: "err"; error: E };

const ok = <T>(value: T): Result<T, never> => ({ tag: "ok", value });
const err = <E>(error: E): Result<never, E> => ({ tag: "err", error });

// バリデーションチェーン
type ValidationError =
  | { code: "EMPTY_NAME"; message: string }
  | { code: "INVALID_EMAIL"; message: string }
  | { code: "UNDERAGE"; message: string };

function validateName(name: string): Result<string, ValidationError> {
  return name.trim().length > 0
    ? ok(name.trim())
    : err({ code: "EMPTY_NAME", message: "名前は必須です" });
}

function validateEmail(email: string): Result<string, ValidationError> {
  return email.includes("@")
    ? ok(email)
    : err({ code: "INVALID_EMAIL", message: "有効なメールではありません" });
}

function validateAge(age: number): Result<number, ValidationError> {
  return age >= 18
    ? ok(age)
    : err({ code: "UNDERAGE", message: "18歳以上である必要があります" });
}

7. モナドを簡単(かんたん)に理解(りかい)する

7.1 モナドとは

モナドは値(あたい)を包(つつ)むコンテキスト(箱(はこ))を持(も)つ型(がた)で、演算(えんざん)をチェーンできるインターフェースを提供(ていきょう)します。

3つの要素(ようそ):

  1. 型コンストラクタ: 値(あたい)を包(つつ)むコンテキスト (Option<T>, Result<T, E>, Promise<T>)
  2. of (return/unit): 値(あたい)をコンテキストに入(い)れる (some(5), ok(5), Promise.resolve(5))
  3. flatMap (bind/chain): コンテキスト内(ない)の値(あたい)に関数(かんすう)を適用(てきよう)し、ネストを解除(かいじょ)
通常のmap:    Option<T>   (TU)Option<U>
flatMap:      Option<T>   (TOption<U>)Option<U>

mapは    箱の中の値を変換
flatMapは 箱の中の値から新しい箱を作り、二重箱を解除

7.2 箱(はこ)の比喩(ひゆ)

┌──────────────────────────────────────────────────┐
│  map: 箱を開けて、変換して、戻す                   │
│                                                  │
[5]  --map(x => x * 2)-->  [10]│                                                  │
│  flatMap: 箱を開けて、新しい箱を作る               │
│                                                  │
[5]  --flatMap(x => [x, x+1])-->  [5, 6]  (二重箱 [[5, 6]] ではない!)│                                                  │
Option例:Some(5) --flatMap(x => Some(x*2))--> Some(10)None    --flatMap(x => Some(x*2))--> None  (Noneなら関数実行自体をスキップ)└──────────────────────────────────────────────────┘

7.3 モナド法則(ほうそく)

// 1. 左単位元 (Left Identity)
// of(a).flatMap(f) === f(a)
flatMap(some(5), double) === double(5)

// 2. 右単位元 (Right Identity)
// m.flatMap(of) === m
flatMap(some(5), some) === some(5)

// 3. 結合法則 (Associativity)
// m.flatMap(f).flatMap(g) === m.flatMap(x => f(x).flatMap(g))

7.4 Promiseはモナドか

// Promise.resolve = of
const p = Promise.resolve(5);

// .then = flatMap (実際にはmapとflatMapの両方の役割)
p.then((x) => x * 2);                        // mapのように
p.then((x) => Promise.resolve(x * 2));        // flatMapのように (自動flatten)

// Promiseは「ほぼ」モナド
// ただし自動flattenのため厳密なモナド法則から若干の差異あり
// Promise<Promise<T>>は不可能 → 常にPromise<T>にフラット化

8. Functor, Applicative, Monadの階層構造(かいそうこうぞう)

8.1 階層(かいそう)ダイアグラム

┌───────────────────────────────────────┐
Monad│  flatMap: F<A>  (AF<B>)F<B>│                                       │
│    ┌───────────────────────────────┐  │
│    │        Applicative            │  │
│    │  ap: F<AB>F<A>F<B>    │  │
│    │                               │  │
│    │    ┌───────────────────────┐  │  │
│    │    │      Functor          │  │  │
│    │    │  map: F<A>  (AB)   │  │  │
│    │    │       → F<B>         │  │  │
│    │    └───────────────────────┘  │  │
│    └───────────────────────────────┘  │
└───────────────────────────────────────┘

8.2 Functor

「マッピングできるもの」— コンテキスト内(ない)の値(あたい)を変換(へんかん)します。

// Array は Functor
[1, 2, 3].map(x => x * 2)  // [2, 4, 6]

// Option は Functor
map(some(5), x => x * 2)   // some(10)
map(none, x => x * 2)      // none

// Promise は Functor
Promise.resolve(5).then(x => x * 2)  // Promise<10>

8.3 Applicative

複数(ふくすう)のコンテキストの値(あたい)に関数(かんすう)を適用(てきよう)します。

const a: Option<number> = some(3);
const b: Option<number> = some(4);

function ap<A, B>(
  ff: Option<(a: A) => B>,
  fa: Option<A>
): Option<B> {
  if (ff.tag === "none" || fa.tag === "none") return none;
  return some(ff.value(fa.value));
}

const add = (x: number) => (y: number) => x + y;
const result = ap(map(a, add), b);  // some(7)

8.4 実践的(じっせんてき)な意味(いみ)

Functor:     単一値の変換        → map
Applicative: 複数独立値の結合    → 並列バリデーション
Monad:       連鎖的依存演算      → 前の結果に基づき次を決定

実践例:
- Functor: ユーザー名を大文字に変換
- Applicative: 名前、メール、年齢を同時検証して結果結合
- Monad: ユーザー検索 → 注文検索 → 決済処理

9. FP実践(じっせん):TypeScript (fp-ts / Effect)

9.1 fp-ts基礎(きそ)

import { pipe, flow } from "fp-ts/function";
import * as O from "fp-ts/Option";
import * as E from "fp-ts/Either";
import * as TE from "fp-ts/TaskEither";

// Option使用
const findUser = (id: string): O.Option<User> =>
  users.has(id) ? O.some(users.get(id)!) : O.none;

const getUserEmail = (id: string): string =>
  pipe(
    findUser(id),
    O.map((u) => u.email),
    O.getOrElse(() => "unknown@example.com")
  );

// Either使用
const parseAge = (input: string): E.Either<string, number> => {
  const age = parseInt(input, 10);
  return isNaN(age)
    ? E.left("無効な年齢")
    : age < 0
      ? E.left("年齢は負数にできません")
      : E.right(age);
};

9.2 TaskEither: 非同期(ひどうき)エラー処理(しょり)

const findUserTE = (id: string): TE.TaskEither<Error, User> =>
  TE.tryCatch(
    () => db.findUser(id),
    (reason) => new Error(`User not found: ${reason}`)
  );

const getUserOrders = (userId: string) =>
  pipe(
    findUserTE(userId),
    TE.chain((user) => findOrderTE(user.id)),
    TE.map((orders) => orders.filter((o) => o.status === "active")),
    TE.fold(
      (error) => async () => ({ error: error.message }),
      (orders) => async () => ({ data: orders })
    )
  );

9.3 Effectライブラリ

Effectはfp-tsの次世代(じせだい)として、より実践的(じっせんてき)で強力(きょうりょく)なFPフレームワークです。

import { Effect, pipe } from "effect";

const divide = (a: number, b: number): Effect.Effect<number, Error> =>
  b === 0
    ? Effect.fail(new Error("Division by zero"))
    : Effect.succeed(a / b);

const program = pipe(
  divide(10, 2),
  Effect.flatMap((result) => divide(result, 3)),
  Effect.map((result) => `Result: ${result}`),
  Effect.catchAll((error) =>
    Effect.succeed(`Error: ${error.message}`)
  )
);

Effect.runPromise(program).then(console.log);

10. FP実践(じっせん):Python

10.1 functools活用(かつよう)

from functools import reduce, partial, lru_cache

# partial: 部分適用
def power(base: int, exponent: int) -> int:
    return base ** exponent

square = partial(power, exponent=2)
cube = partial(power, exponent=3)

print(square(5))  # 25
print(cube(3))    # 27

# lru_cache: メモ化(純粋関数に最適)
@lru_cache(maxsize=128)
def fibonacci(n: int) -> int:
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

10.2 returnsライブラリ

from returns.result import Result, Success, Failure
from returns.maybe import Maybe, Some, Nothing

# Maybe使用
def find_user(user_id: str) -> Maybe[dict]:
    users = {"u1": {"name": "Alice", "email": "alice@example.com"}}
    return Some(users[user_id]) if user_id in users else Nothing

def get_email(user: dict) -> Maybe[str]:
    email = user.get("email")
    return Some(email) if email else Nothing

result = find_user("u1").bind(get_email)    # Some("alice@example.com")
result = find_user("u999").bind(get_email)  # Nothing

# Result使用
def parse_int(value: str) -> Result[int, str]:
    try:
        return Success(int(value))
    except ValueError:
        return Failure(f"Cannot parse '{value}' as int")

def validate_positive(n: int) -> Result[int, str]:
    return Success(n) if n > 0 else Failure("Must be positive")

result = parse_int("42").bind(validate_positive)   # Success(42)
result = parse_int("-1").bind(validate_positive)    # Failure("Must be positive")

11. FP vs OOP

11.1 比較表(ひかくひょう)

┌──────────────────┬──────────────────────┬──────────────────────┐
│     観点          │         OOPFP├──────────────────┼──────────────────────┼──────────────────────┤
│ 基本単位         │ オブジェクト(状態+行動)関数(変換)│ 状態管理         │ 可変状態カプセル化    │ 不変データ変換        │
│ 多態性           │ 継承/インターフェース │ 型クラス/パターンマッチ│
│ コード再利用     │ 継承                  │ 合成                 │
│ 副作用           │ どこでも許可          │ 分離/管理            │
│ 並行性           │ ロック/同期が必要     │ 不変性で安全          │
│ エラー処理       │ 例外 (try-catch)Result / Either│ デザインパターン  │ GoFパターン          │ モナド、ファンクタ等  │
│ テスト           │ Mock/DI必要          │ 純粋関数直接テスト    │
│ 学習曲線         │ 比較的低い            │ 比較的高い           │
└──────────────────┴──────────────────────┴──────────────────────┘

11.2 ハイブリッドアプローチ

現代(げんだい)の実践(じっせん)では、純粋(じゅんすい)なFPや純粋(じゅんすい)なOOPではなく、ハイブリッドアプローチが最(もっと)も効果的(こうかてき)です。

class OrderService {
  constructor(
    private readonly db: Database,
    private readonly logger: Logger
  ) {}

  // 純粋関数: ビジネスロジック
  private static calculateTotal(items: ReadonlyArray<OrderItem>): number {
    return items.reduce((sum, item) => sum + item.price * item.qty, 0);
  }

  private static applyDiscount(total: number, discountRate: number): number {
    return total * (1 - discountRate);
  }

  // 不純なシェル: I/O
  async processOrder(orderId: string): Promise<Result<Order, Error>> {
    const order = await this.db.findOrder(orderId);
    if (!order) return err(new Error("Order not found"));

    const total = OrderService.calculateTotal(order.items);
    const finalTotal = OrderService.applyDiscount(total, order.discountRate);

    const updated = { ...order, total: finalTotal, status: "processed" as const };
    await this.db.saveOrder(updated);
    this.logger.info(`Order ${orderId} processed: ${finalTotal}`);

    return ok(updated);
  }
}

12. FPパターン:バックエンド実践(じっせん)

12.1 Railway Oriented Programming

成功(せいこう)/失敗(しっぱい)パスを2本(ほん)のレールで視覚化(しかくか)するエラー処理(しょり)パターンです。

成功レール: ──── validate ──── transform ──── save ──── OK
                    │                │           │
失敗レール: ────────┼────────────────┼───────────┼──── ERR
                    │                │           │
              検証失敗          変換エラー    保存失敗
type Railway<T, E> = Result<T, E>;

function railway<A, B, E>(
  fn: (a: A) => Railway<B, E>
): (input: Railway<A, E>) => Railway<B, E> {
  return (input) => {
    if (input.tag === "err") return input;
    return fn(input.value);
  };
}

// 実践: ユーザー登録パイプライン
const registerUser = flow(
  validateInput,
  railway(checkDuplicate),
  railway(hashPassword),
  railway(saveToDb),
  railway(sendWelcomeEmail),
);

12.2 イベントソーシングとFP

イベントソーシングは本質的(ほんしつてき)に関数型(かんすうがた)パターンです:fold(events, initialState) = currentState

type AccountEvent =
  | { type: "ACCOUNT_OPENED"; owner: string; initialBalance: number }
  | { type: "DEPOSITED"; amount: number }
  | { type: "WITHDRAWN"; amount: number }
  | { type: "CLOSED" };

interface AccountState {
  owner: string;
  balance: number;
  status: "active" | "closed";
}

const initialState: AccountState = {
  owner: "",
  balance: 0,
  status: "active",
};

// リデューサー: (State, Event) => State (純粋関数!)
function accountReducer(
  state: AccountState,
  event: AccountEvent
): AccountState {
  switch (event.type) {
    case "ACCOUNT_OPENED":
      return {
        owner: event.owner,
        balance: event.initialBalance,
        status: "active",
      };
    case "DEPOSITED":
      return { ...state, balance: state.balance + event.amount };
    case "WITHDRAWN":
      return { ...state, balance: state.balance - event.amount };
    case "CLOSED":
      return { ...state, status: "closed" };
  }
}

const events: AccountEvent[] = [
  { type: "ACCOUNT_OPENED", owner: "Alice", initialBalance: 1000 },
  { type: "DEPOSITED", amount: 500 },
  { type: "WITHDRAWN", amount: 200 },
];

const currentState = events.reduce(accountReducer, initialState);
// { owner: "Alice", balance: 1300, status: "active" }

13. 面接質問(めんせつしつもん)15選(せん)

基礎(きそ)(1-5)

Q1. 純粋関数(じゅんすいかんすう)とは何(なに)か、なぜ重要(じゅうよう)ですか?

回答を見る

純粋関数(じゅんすいかんすう)は(1)同(おな)じ入力(にゅうりょく)に常(つね)に同(おな)じ出力(しゅつりょく)を返(かえ)し、(2)副作用(ふくさよう)がない関数(かんすう)です。テストが容易(ようい)で、推論(すいろん)が簡単(かんたん)であり、参照透過性(さんしょうとうかせい)を保証(ほしょう)して関数合成(かんすうごうせい)や最適化(さいてきか)(メモ化(か)など)が可能(かのう)だからです。

Q2. 不変性(ふへんせい)が並行(へいこう)プログラミングに役立(やくだ)つ理由(りゆう)は?

回答を見る

不変(ふへん)データは複数(ふくすう)のスレッドから同時(どうじ)に読(よ)み取(と)っても安全(あんぜん)です。データが変更(へんこう)されないのでrace conditionが発生(はっせい)せず、ロックが不要(ふよう)です。各(かく)スレッドは独自(どくじ)の新(あたら)しいコピーを作成(さくせい)するため、他(ほか)のスレッドに影響(えいきょう)を与(あた)えません。

Q3. mapとflatMapの違(ちが)いを説明(せつめい)してください。

回答を見る

mapはコンテナ内(ない)の値(あたい)に関数(かんすう)を適用(てきよう)して変換(へんかん)します。flatMapは関数(かんすう)がコンテナを返(かえ)す場合(ばあい)にネストを解除(かいじょ)します。例(たと)えばOption(Option(5))ではなくOption(5)を返(かえ)します。

Q4. 高階関数(こうかいかんすう)の例(れい)を3つ挙(あ)げてください。

回答を見る

(1) Array.map - 関数(かんすう)を引数(ひきすう)として受(う)け取(と)り各要素(ようそ)に適用(てきよう)。(2) Array.filter - 条件関数(じょうけんかんすう)を受(う)け取(と)り真(しん)の要素(ようそ)のみ返却(へんきゃく)。(3) Array.sort(compareFn) - 比較関数(ひかくかんすう)を受(う)け取(と)りソート。関数(かんすう)を返(かえ)す例(れい)としてはカリー化(か)、ファクトリパターンがあります。

Q5. クロージャと純粋関数(じゅんすいかんすう)の関係(かんけい)は?

回答を見る

クロージャは外部変数(がいぶへんすう)をキャプチャする関数(かんすう)です。キャプチャした変数(へんすう)が不変(ふへん)であればクロージャも純粋関数(じゅんすいかんすう)になりえます。しかしキャプチャした変数(へんすう)が可変(かへん)であれば副作用(ふくさよう)が生(しょう)じ純粋関数(じゅんすいかんすう)ではなくなります。

中級(ちゅうきゅう)(6-10)

Q6. モナドを非開発者(ひかいはつしゃ)にどう説明(せつめい)しますか?

回答を見る

モナドは「値(あたい)を入(い)れる特別(とくべつ)な箱(はこ)」と比喩(ひゆ)できます。この箱(はこ)は(1)値(あたい)を入(い)れることができ(of)、(2)箱(はこ)の中(なか)の値(あたい)を変換(へんかん)でき(map)、(3)変換結果(へんかんけっか)がまた別(べつ)の箱(はこ)の場合(ばあい)、二重箱(にじゅうはこ)を解除(かいじょ)します(flatMap)。例(たと)えばPromiseは「未来(みらい)の値(あたい)を入(い)れる箱(はこ)」で、.then()で変換(へんかん)しながらネストされたPromiseを自動的(じどうてき)に解除(かいじょ)します。

Q7. Option/Maybe型(がた)がnullより優(すぐ)れている理由(りゆう)は?

回答を見る

(1)コンパイル時(じ)にnull可能性(かのうせい)の処理(しょり)を強制(きょうせい)します。(2)チェーンが可能(かのう)でネストされたnullチェックが消(き)えます。(3)「値(あたい)がないかもしれない」ことを明示的(めいじてき)に型(がた)で表現(ひょうげん)します。(4)map/flatMapでnull伝播(でんぱ)が自動的(じどうてき)に行(おこな)われます。

Q8. Railway Oriented Programmingを説明(せつめい)してください。

回答を見る

エラー処理(しょり)を2本(ほん)のレール(成功(せいこう)/失敗(しっぱい))で視覚化(しかくか)するパターンです。各関数(かくかんすう)はResult/Eitherを返(かえ)し、成功(せいこう)なら次(つぎ)の関数(かんすう)へ、失敗(しっぱい)なら即座(そくざ)にエラーレールに切(き)り替(か)わります。try-catchブロックを排除(はいじょ)し、エラー伝播(でんぱ)を関数合成(かんすうごうせい)で処理(しょり)します。

Q9. イベントソーシングと関数型(かんすうがた)プログラミングの関連(かんれん)は?

回答を見る

イベントソーシングの核心(かくしん)はfold(events, initialState) = currentStateであり、純粋関数(じゅんすいかんすう)であるリデューサーがイベントストリームを初期状態(しょきじょうたい)に順次適用(じゅんじてきよう)して現在(げんざい)の状態(じょうたい)を導出(どうしゅつ)します。FPの不変性(ふへんせい)、純粋関数(じゅんすいかんすう)、reduceパターンの直接的(ちょくせつてき)な適用(てきよう)です。

Q10. カリー化(か)と部分適用(ぶぶんてきよう)の違(ちが)いは?

回答を見る

カリー化(か)はn引数関数(ひきすうかんすう)をn個(こ)の1引数関数(ひきすうかんすう)チェーンに変換(へんかん)します:f(a, b, c)f(a)(b)(c)になります。部分適用(ぶぶんてきよう)は一部(いちぶ)の引数(ひきすう)を固定(こてい)して残(のこ)りの引数(ひきすう)を受(う)け取(と)る新(あたら)しい関数(かんすう)を作成(さくせい)します。カリー化(か)された関数(かんすう)は自然(しぜん)に部分適用(ぶぶんてきよう)をサポートします。

上級(じょうきゅう)(11-15)

Q11. fp-tsにおけるTaskEitherの役割(やくわり)は?

回答を見る

TaskEitherは「失敗(しっぱい)する可能性(かのうせい)のある非同期(ひどうき)演算(えんざん)」を型(がた)で表現(ひょうげん)します。try-catchの代(か)わりに型安全(かたあんぜん)なエラー処理(しょり)を提供(ていきょう)し、chain/map/foldで非同期(ひどうき)パイプラインを合成(ごうせい)できます。DB検索(けんさく)、API呼(よ)び出(だ)しなど失敗(しっぱい)可能(かのう)なI/O操作(そうさ)に適(てき)しています。

Q12. Functor, Applicative, Monadの違(ちが)いを説明(せつめい)してください。

回答を見る

FunctorはmapをもつType(コンテキスト内(ない)の値(あたい)変換(へんかん))。Applicativeは複数(ふくすう)コンテキストの値(あたい)に関数(かんすう)を適用(てきよう)(並列(へいれつ)独立(どくりつ)演算(えんざん)の結合(けつごう))。Monadは前(まえ)の結果(けっか)に基(もと)づき次(つぎ)の演算(えんざん)を決定(けってい)(flatMap: 順次依存演算(じゅんじいぞんえんざん))。階層的(かいそうてき)にMonadが最(もっと)も強力(きょうりょく)で、すべてのMonadはApplicativeであり、すべてのApplicativeはFunctorです。

Q13. 不変(ふへん)データ構造(こうぞう)のパフォーマンス問題(もんだい)はどう解決(かいけつ)しますか?

回答を見る

(1)構造的共有(こうぞうてききょうゆう):変更部分(へんこうぶぶん)のみ新規作成(しんきさくせい)、残(のこ)りは参照共有(さんしょうきょうゆう)。(2)永続(えいぞく)データ構造(こうぞう):Trie基盤(きばん)でO(log32 N)更新(こうしん)。(3)COW(Copy-on-Write):実際(じっさい)の変更時(へんこうじ)のみコピー(Immerなど)。(4)Transient:構築中(こうちくちゅう)は可変(かへん)、完成後(かんせいご)に不変(ふへん)に転換(てんかん)。

Q14. ADTで不可能(ふかのう)な状態(じょうたい)をどう排除(はいじょ)しますか?

回答を見る

booleanフラグの組(く)み合(あ)わせの代(か)わりにDiscriminated Unionで可能(かのう)な状態(じょうたい)のみを明示(めいじ)します。例(たと)えばisLoading, isError, data, error の4フィールドの代(か)わりに Idle, Loading, Success(data), Error(error) の4状態(じょうたい)を定義(ていぎ)すれば、「読(よ)み込(こ)み中(ちゅう)かつエラー」のような不可能(ふかのう)な状態(じょうたい)が型(がた)レベルで遮断(しゃだん)されます。

Q15. 既存(きぞん)のOOPコードベースにFPを段階的(だんかいてき)に導入(どうにゅう)する方法(ほうほう)は?

回答を見る

(1)まず純粋関数(じゅんすいかんすう)でユーティリティ/ヘルパーを作成(さくせい)。(2)不変(ふへん)データパターン導入(どうにゅう)(readonly, Object.freeze, Immer)。(3)ビジネスロジックをFunctional Core / Imperative Shellパターンで分離(ぶんり)。(4)Result/Option型(がた)でエラー処理(しょり)を改善(かいぜん)。(5)パイプラインパターンでデータ変換(へんかん)フローを整理(せいり)。全面的(ぜんめんてき)な転換(てんかん)ではなく段階的(だんかいてき)な採用(さいよう)が核心(かくしん)です。


14. クイズ5選(せん)

クイズ1

次(つぎ)のうち純粋関数(じゅんすいかんすう)でないものは?

A) const add = (a, b) => a + b B) const now = () => Date.now() C) const double = (x) => x * 2 D) const head = (arr) => arr[0]

回答を見る

B) Date.now()は呼(よ)び出(だ)すたびに異(こと)なる値(あたい)を返(かえ)すので非決定論的(ひけっていろんてき)であり、純粋関数(じゅんすいかんすう)ではありません。

クイズ2

次(つぎ)のコードの出力(しゅつりょく)は?

const arr = [1, 2, 3, 4, 5];
const result = arr
  .filter(x => x % 2 === 0)
  .map(x => x * 3)
  .reduce((acc, x) => acc + x, 0);

A) 15 B) 18 C) 30 D) 6

回答を見る

B) 18。filterで[2, 4]、mapで[6, 12]、reduceで 6 + 12 = 18。

クイズ3

モナドの左単位元(ひだりたんいげん)(Left Identity)法則(ほうそく)は?

A) m.flatMap(of) === m B) of(a).flatMap(f) === f(a) C) m.flatMap(f).flatMap(g) === m.flatMap(x => f(x).flatMap(g)) D) m.map(id) === m

回答を見る

B) 左単位元(ひだりたんいげん)は値(あたい)をコンテキストに入(い)れて(of)flatMapすると、関数(かんすう)に直接値(ちょくせつあたい)を入(い)れたのと同(おな)じという法則(ほうそく)です。Aは右単位元(みぎたんいげん)、Cは結合法則(けつごうほうそく)、DはFunctor法則(ほうそく)です。

クイズ4

次(つぎ)のTypeScriptコードでresultの型(がた)と値(あたい)は?

import * as O from "fp-ts/Option";
import { pipe } from "fp-ts/function";

const result = pipe(
  O.some(5),
  O.map(x => x * 2),
  O.flatMap(x => x > 5 ? O.some(x) : O.none),
);

A) Option<number> 値(あたい) some(10) B) Option<number> 値(あたい) none C) number 値(あたい) 10 D) コンパイルエラー

回答を見る

A) some(5)をmapでsome(10)に変換(へんかん)、flatMapで10は5より大(おお)きいのでsome(10)を返却(へんきゃく)。型(がた)はOption<number>、値(あたい)はsome(10)です。

クイズ5

Functional Core / Imperative Shellパターンについて正(ただ)しい説明(せつめい)は?

A) Functional CoreにDBアクセスロジックを含(ふく)む B) Imperative Shellでビジネスルールを計算(けいさん)する C) Functional Coreは純粋関数(じゅんすいかんすう)で構成(こうせい)し、ShellはI/Oを担当(たんとう)する D) Shellを先(さき)に作成(さくせい)してCoreは後(あと)でリファクタリングする

回答を見る

C) Functional Coreはビジネスロジック(計算(けいさん)、検証(けんしょう)、変換(へんかん))を純粋関数(じゅんすいかんすう)で処理(しょり)し、Imperative ShellはDB、ネットワーク、ファイル等(など)のI/Oを担当(たんとう)します。Coreはテストが容易(ようい)で再利用可能(さいりようかのう)、Shellは可能(かのう)な限(かぎ)り薄(うす)く保(たも)ちます。


15. 参考資料(さんこうしりょう)

書籍(しょせき)

  • Structure and Interpretation of Computer Programs (SICP) - Harold Abelson, Gerald Jay Sussman
  • Functional Programming in Scala - Paul Chiusano, Runar Bjarnason
  • Category Theory for Programmers - Bartosz Milewski
  • Domain Modeling Made Functional - Scott Wlaschin
  • Grokking Simplicity - Eric Normand

オンラインドキュメント

ブログ・記事(きじ)

ツール・ライブラリ

  • fp-ts - TypeScript FPライブラリ
  • Effect - 次世代TypeScript FPフレームワーク
  • Immer - JavaScript不変性ライブラリ
  • returns - Python FPライブラリ
  • Ramda - 実用JavaScript FPライブラリ

コミュニティ