メインコンテンツまでスキップ
バージョン: 0.5.0 (最新)

🧮 関数型プログラミングコンセプト

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) {
// どのような種類のエラーか?予期されるか予期されないか?
}

➡️ 次へ