🧮 関数型プログラミングコンセプト
NodeBlocksバックエンドSDKは関数型プログラミングの原則に基づいて構築されています。これらの数学的概念を理解することで、より優雅で合成可能で保守可能なコードを書けるようになります。
🔍 関数型プログラミングとは何ですか?
関数型プログラミングは、計算を数学関数の評価として扱うプログラミングパラダイムです。NodeBlocksでは、関数型プログラミングを使用して以下を実現します:
- 合成 シンプルな関数から複雑な操作を作成
- 回避 可変状態と副作用を避ける
- 作成 予測可能でテスト可能なコード
- 構築 モジュール化され、再利用可能なコンポーネント
📐 コア数学的概念
関数合成
関数合成は、2つの関数を組み合わせて3番目の関数を生成する数学的操作です。
数学的定義:
(f ∘ g)(x) = f(g(x))
NodeBlocksでは:
import { compose } from '@nodeblocks/backend-sdk';
// 代わりに:
const result = f(g(x));
// 合成を使用:
const composedFunction = compose(f, g);
const result = composedFunction(x);
実際の例:
// ユーザー機能を作成: 検証 → 保存 → レスポンスフォーマット
const createUserFeature = compose(
validateUserSchema, // f: 入力を検証
createUserHandler, // g: データベースに保存
formatUserResponse // h: 出力をフォーマット
);
// これは formatUserResponse(createUserHandler(validateUserSchema(input))) と同等
利点:
- 読みやすい: 関数が左から右に流れる
- 合成可能: ステップの追加/削除が簡単
- テスト可能: 各関数を独立してテスト可能
Currying
カリー化は、多引数関数を各々が単一引数を取る関数シーケンスに変換するテクニックです。
数学的定義:
f(x, y, z) → f(x)(y)(z)
NodeBlocksでは:
import { curry } from 'ramda';
// 通常の関数
const add = (a, b) => a + b;
// カリー化された関数
const curriedAdd = curry(add);
const addFive = curriedAdd(5);
const result = addFive(3); // 8
実際の例 - 認証バリデーション:
// カリー化なし(すべての引数を一度に必要)
const verifyAuth = (authFunction, payload) => {
return authFunction(payload);
};
// カリー化あり(部分適用可能)
const verifyAuthentication = curry((authFunction, payload) => {
return authFunction(payload);
});
// 使用方法: 認証関数を部分適用
const validateWithJWT = verifyAuthentication(jwtAuthFunction);
// ルートで使用
const protectedRoute = withRoute({
validators: [validateWithJWT], // 今はペイロードのみ必要
handler: secretHandler
});
利点:
- 部分適用: 特殊な関数を作成
- 再利用性: 同じ関数、異なる設定
- 合成可能性: 他の関数との組み合わせが簡単
部分適用
部分適用は、関数の引数をいくつか固定し、より少ない引数の関数を生成するプロセスです。
数学的定義:
f(x, y, z) → f(x, y, _) → g(z)
NodeBlocksでは:
import _ from 'lodash';
// 元の関数
const multiply = (a, b) => a * b;
// 最初の引数を部分適用
const multiplyByTwo = _.partial(multiply, 2);
const result = multiplyByTwo(5); // 10
実際の例 - サービス設定:
// サービスはデータベースと設定を必要とする
const authService = (db, config) => {
return defService(_.partial(compose(feature1, feature2), { dataStores: db, ...config }));
};
// 設定を部分適用
const authServiceWithConfig = _.partial(authService, _, {
maxFailedLoginAttempts: 5,
accessTokenExpireTime: '2h'
});
// 今はデータベースのみ必要
app.use('/auth', authServiceWithConfig(database));
利点:
- 設定: デフォルトパラメータで関数を設定
- 柔軟性: 同じ関数、異なる設定
- クリーンなコード: 繰り返しを減らす
高階関数
高階関数は、関数を引数として受け取ったり、関数を結果として返したりする関数です。
数学的定義:
H(f) = g, ここでfとgは関数
NodeBlocksでは:
// 関数を返す関数
const createValidator = (errorMessage) => {
return (value) => {
if (!value) {
throw new NodeblocksError(400, errorMessage);
}
};
};
// 使用方法
const requireUserId = createValidator('User ID is required');
const requireEmail = createValidator('Email is required');
実際の例 - ルートファクトリ:
// ルートを作成する高階関数
const createCRUDRoute = (handler, validators = []) => {
return withRoute({
handler,
validators,
method: 'POST',
path: '/items'
});
};
// 使用方法
const createUserRoute = createCRUDRoute(createUserHandler, [validateUser]);
const createProductRoute = createCRUDRoute(createProductHandler, [validateProduct]);
🔧 NodeBlocks関数型パターン
Result型(モナド)
NodeBlocksは、Result
型を使用して成功と失敗のケースを明示的に処理します。
import { Result, ok, err } from 'neverthrow';
// 関数はthrowする代わりにResultを返す
const validateUser = (data): Result<User, ValidationError> => {
if (!data.email) {
return err(new ValidationError('Email required'));
}
return ok(new User(data));
};
const saveUser = async (user: User): Promise<Result<User, DatabaseError>> => {
try {
const saved = await db.users.insert(user);
return ok(saved);
} catch (error) {
return err(new DatabaseError(error.message));
}
};
// flatMapで合成
const createUser = compose(
validateUser,
flatMapAsync(saveUser)
);
関数リフティング
リフティングは、通常の関数をResultとともに動作するように変換します。
import { lift } from '@nodeblocks/backend-sdk';
// 通常の関数
const formatResponse = (user) => ({
success: true,
data: user
});
// リフティングされた関数はResultとともに動作
const formatResponseLifted = lift(formatResponse);
// 合成での使用
const createUserFeature = compose(
validateUser,
flatMapAsync(saveUser),
lift(formatResponse) // 通常の関数をResultとともに動作するようにリフト
);
パイプライン合成
NodeBlocksは、パイプラインを使用して操作を連鎖させます。
// パイプライン: 入力 → 検証 → ビジネスロジック → レスポンス
const userFeature = compose(
// 1. 入力を検証
validateUserSchema,
// 2. ビジネスロジック
flatMapAsync(createUser),
flatMapAsync(sendWelcomeEmail),
// 3. レスポンスをフォーマット
lift(formatUserResponse)
);
🧮 数学的基礎
圏論
NodeBlocksのパターンは、圏論の概念に着想を得ています:
関手(Functors):
// Resultは関手 - マップ可能
const userResult = ok({ name: 'John', email: 'john@example.com' });
const formattedResult = userResult.map(user => ({
...user,
displayName: user.name.toUpperCase()
}));
モナド(Monads):
// Resultはモナド - チェーン可能
const result = ok(5)
.andThen(x => ok(x * 2))
.andThen(x => ok(x + 1));
// 結果: ok(11)
代数的データ型
NodeBlocksは、モデリングに代数的データ型を使用します:
// 和型(union)
type ValidationResult =
| { type: 'success'; data: User }
| { type: 'error'; message: string };
// 積型(tuple/object)
type RouteConfig = {
method: string;
path: string;
handler: Function;
validators: Function[];
};
📐 ベストプラクティス
1. 純粋関数
- 関数は副作用を持たない
- 同じ入力は常に同じ出力を生成
- テストしやすく、推論しやすい
// ✅ 純粋関数
const add = (a, b) => a + b;
// ❌ 不純関数(副作用)
const addAndLog = (a, b) => {
console.log('Adding:', a, b); // 副作用
return a + b;
};
2. 不変性
- 既存のデータを変更しない
- 新しいデータ構造を作成する
// ✅ 不変
const updateUser = (user, updates) => ({
...user,
...updates
});
// ❌ 可変
const updateUser = (user, updates) => {
Object.assign(user, updates); // 元を変更
return user;
};
3. 関数合成
- シンプルな関数から複雑な操作を構築
- 関数を集中し、単一目的に保つ
// ✅ 合成済み
const processUser = compose(
validateUser,
flatMapAsync(saveUser),
lift(formatResponse)
);
// ❌ 一枚岩
const processUser = async (data) => {
// 100 lines of mixed concerns
};
4. Resultを使用したエラーハンドリング
- 予期されるエラーには例外の代わりにResultを使用
- エラーハンドリングを明示的にする
// ✅ 明示的なエラーハンドリング
const result = await createUser(userData);
if (result.isErr()) {
return handleError(result.error);
}
return result.value;
// ❌ 暗黙的なエラーハンドリング
try {
const user = await createUser(userData);
return user;
} catch (error) {
// どのような種類のエラーか?予期されるか予期されないか?
}