- Authors

- Name
- Youngju Kim
- @fjvbn20031
- はじめに:なぜ2025年(ねん)に関数型(かんすうがた)プログラミングなのか
- 1. 不変性(ふへんせい)(Immutability)
- 2. 純粋関数(じゅんすいかんすう)(Pure Functions)
- 3. 高階関数(こうかいかんすう)(Higher-Order Functions)
- 4. クロージャとカリー化(か)
- 5. 代数的(だいすうてき)データ型(がた)(Algebraic Data Types)
- 6. Option/MaybeとResult/Either
- 7. モナドを簡単(かんたん)に理解(りかい)する
- 8. Functor, Applicative, Monadの階層構造(かいそうこうぞう)
- 9. FP実践(じっせん):TypeScript (fp-ts / Effect)
- 10. FP実践(じっせん):Python
- 11. FP vs OOP
- 12. FPパターン:バックエンド実践(じっせん)
- 13. 面接質問(めんせつしつもん)15選(せん)
- 14. クイズ5選(せん)
- 15. 参考資料(さんこうしりょう)
はじめに:なぜ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 Matching │
│ Python 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 = 100 │
│ Thread 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は共有(コピー不要)
→ A→A', C→C', F→F'のみ新規作成
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つの条件(じょうけん)を満(み)たします:
- 決定論的(けっていろんてき): 同(おな)じ入力(にゅうりょく)に常(つね)に同(おな)じ出力(しゅつりょく)
- 副作用(ふくさよう)なし: 外部(がいぶ)状態(じょうたい)を変更(へんこう)しない
// 純粋関数
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つの要素(ようそ):
- 型コンストラクタ: 値(あたい)を包(つつ)むコンテキスト (
Option<T>,Result<T, E>,Promise<T>) - of (return/unit): 値(あたい)をコンテキストに入(い)れる (
some(5),ok(5),Promise.resolve(5)) - flatMap (bind/chain): コンテキスト内(ない)の値(あたい)に関数(かんすう)を適用(てきよう)し、ネストを解除(かいじょ)
通常のmap: Option<T> → (T → U) → Option<U>
flatMap: Option<T> → (T → Option<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> → (A → F<B>) → F<B> │
│ │
│ ┌───────────────────────────────┐ │
│ │ Applicative │ │
│ │ ap: F<A→B> → F<A> → F<B> │ │
│ │ │ │
│ │ ┌───────────────────────┐ │ │
│ │ │ Functor │ │ │
│ │ │ map: F<A> → (A→B) │ │ │
│ │ │ → 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 比較表(ひかくひょう)
┌──────────────────┬──────────────────────┬──────────────────────┐
│ 観点 │ OOP │ FP │
├──────────────────┼──────────────────────┼──────────────────────┤
│ 基本単位 │ オブジェクト(状態+行動)│ 関数(変換) │
│ 状態管理 │ 可変状態カプセル化 │ 不変データ変換 │
│ 多態性 │ 継承/インターフェース │ 型クラス/パターンマッチ│
│ コード再利用 │ 継承 │ 合成 │
│ 副作用 │ どこでも許可 │ 分離/管理 │
│ 並行性 │ ロック/同期が必要 │ 不変性で安全 │
│ エラー処理 │ 例外 (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
オンラインドキュメント
ブログ・記事(きじ)
- Mostly Adequate Guide to FP (in JS)
- Learn You a Haskell for Great Good
- fp-ts学習シリーズ
- returnsライブラリドキュメント
- Why Functional Programming Matters - John Hughes
ツール・ライブラリ
- fp-ts - TypeScript FPライブラリ
- Effect - 次世代TypeScript FPフレームワーク
- Immer - JavaScript不変性ライブラリ
- returns - Python FPライブラリ
- Ramda - 実用JavaScript FPライブラリ