🧮 関数型プログラミングの概念
NodeblocksバックエンドSDKは関数型プログラミングの原則に基づいて構築されています。これらの数学的概念を理解することで、よりエレガントで、コンポーズ可能で、保守しやすいコードを書けるようになります。
🔍 関数型プログラミングとは?
関数型プログラミングは、計算を数学的関数の評価として扱うプログラミングパラダイムです。Nodeblocksでは、関数型プログラミングを使用して以下を行います:
- コンポーズ: シンプルな関数から複雑な操作を作成する
- 回避: 可変状態と副作用を避ける
- 作成: 予測可能でテスト可能なコードを作成する
- 構築: モジュール化された再利用可能なコンポーネントを構築する
📐 コア数学的概念
関数コンポジション
関数コンポジションは、2つの関数を組み合わせて3番目の関数を生成する数学的操作です。
数学的定義:
(f ∘ g)(x) = f(g(x))
Nodeblocksでは:
import { primitives } from '@nodeblocks/backend-sdk';
const { compose } = primitives;
// 以下の代わりに:
const result = f(g(x));
// コンポジションを使用:
const composedFunction = compose(f, g);
const result = composedFunction(x);
実際の例:
import { primitives } from '@nodeblocks/backend-sdk';
const { compose } = primitives;
// ユーザー機能を作成: バリデート → 保存 → レスポンスをフォーマット
const createUserFeature = compose(
validateUserSchema, // f: 入力をバリデート
createUserHandler, // g: データベースに保存
formatUserResponse // h: 出力をフォーマット
);
// これは以下と同等: formatUserResponse(createUserHandler(validateUserSchema(input)))
利点:
- 読みやすい: 関数が左から右に流れる
- コンポーズ可能: ステップの追加/削除が容易
- テスト可能: 各関数を独立してテストできる
カリー化
カリー化は、複数の引数を取る関数を、それぞれが単一の引数を取る関数のシーケンスに変換する手法です。
数学的定義:
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
実際の例 - 認証バリデーター:
import { curry } from 'ramda';
import { primitives } from '@nodeblocks/backend-sdk';
const { withRoute } = primitives;
// カリー化なし(すべての引数を一度に必要とする)
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, where f and g are functions
Nodeblocksでは:
import { primitives } from '@nodeblocks/backend-sdk';
// 関数を返す関数
const createValidator = (errorMessage) => {
return (value) => {
if (!value) {
throw new primitives.NodeblocksError(400, errorMessage);
}
};
};
// 使用
const requireUserId = createValidator('User ID is required');
const requireEmail = createValidator('Email is required');
実際の例 - ルートファクトリー:
import { primitives } from '@nodeblocks/backend-sdk';
const { withRoute } = primitives;
// ルートを作成する高階関数
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';
import { primitives } from '@nodeblocks/backend-sdk';
const { compose, flatMapAsync } = primitives;
// 関数はスローする代わりに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 { primitives } from '@nodeblocks/backend-sdk';
const { compose, flatMapAsync, lift } = primitives;
// 通常の関数
const formatResponse = (user) => ({
success: true,
data: user
});
// リフティングされた関数はResultで動作する
const formatResponseLifted = lift(formatResponse);
// コンポジションでの使用
const createUserFeature = compose(
validateUser,
flatMapAsync(saveUser),
lift(formatResponse) // 通常の関数をResultで動作するようにリフティング
);
パイプラインコンポジション
Nodeblocksはパイプラインを使用して操作をチェーンします。
import { primitives } from '@nodeblocks/backend-sdk';
const { compose, flatMapAsync, lift } = primitives;
// パイプライン: 入力 → バリデーション → ビジネスロジック → レスポンス
const userFeature = compose(
// 1. 入力をバリデート
validateUserSchema,
// 2. ビジネスロジック
flatMapAsync(createUser),
flatMapAsync(sendWelcomeEmail),
// 3. レスポンスをフォーマット
lift(formatUserResponse)
);
🧮 数学的基盤
圏論
Nodeblocksパターンは圏論の概念にインスパイアされています:
ファンクター:
// Resultはファンクター - マップできる
const userResult = ok({ name: 'John', email: 'john@example.com' });
const formattedResult = userResult.map(user => ({
...user,
displayName: user.name.toUpperCase()
}));
モナド:
// Resultはモナド - チェーンできる
const result = ok(5)
.andThen(x => ok(x * 2))
.andThen(x => ok(x + 1));
// 結果: ok(11)
📐 ベストプラクティス
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. 関数コンポジション
- シンプルな関数から複雑な操作を構築する
- 関数を焦点を絞った単一目的に保つ
import { primitives } from '@nodeblocks/backend-sdk';
const { compose, flatMapAsync, lift } = primitives;
// ✅ コンポーズされた
const processUser = compose(
validateUser,
flatMapAsync(saveUser),
lift(formatResponse)
);
// ❌ モノリシック
const processUser = async (data) => {
// 100行の混在した関心事
};
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) {
// どのタイプのエラー?予期されるものか予期しないものか?
}
➡️ 次へ
- ルートコンポジションについて学び、これらの概念を実際に見る
- 関数型サービス設計のためのサービスパターンを探索する
- 関数型バリデーションパターンのバリデーターを確認する