🧮 関数型プログラミングコンセプト
NodeblocksバックエンドSDKは関数型の原則の上に構築されています。これらの数学的コンセプトを理解すると、よりエレガントで合成可能かつ保守可能なコードを書くことができます。
🔍 関数型プログラミングとは?
関数型プログラミングは、計算を数学的関数の評価として扱うプログラミングパラダイムです。Nodeblocksでは、以下の目的で関数型プログラミングを使用します:
- 単純な関数から複雑な操作を合成する
- ミュータブルな状態を最小限に抑え、サイドエフェクト(DB I/O、ロギング)をブロックとハンドラに隔離する
- 予測可能でテスト可能なコードを作成する
- モジュール化和再利用可能なコンポーネントを構築する
📐 コアの数学的コンセプト
関数合成
関数合成は複数の関数を1つのパイプラインに組み合わせます。SDKにおいて、compose はRamdaの pipe のエイリアスです — 関数は左から右へ実行されます:
compose(f, g, h)(x) = h(g(f(x)))
Nodeblocksでの使用:
import { primitives } from '@nodeblocks/backend-sdk';
const { compose } = primitives;
// ネストされた呼び出しの代わりに:
const result = h(g(f(x)));
// パイプラインを使用します:
const pipeline = compose(f, g, h);
const result = pipeline(x);
SDK例 — フィーチャー合成:
import { primitives, routes, schemas } from '@nodeblocks/backend-sdk';
const { compose } = primitives;
const { registerCredentialsSchema } = schemas;
const { registerCredentialsRoute } = routes;
// フィーチャーは左から右へ合成されます:スキーマ → ルート
const registerCredentialsFeature = compose(
registerCredentialsSchema, // 1. OpenAPIスキーマ登録
registerCredentialsRoute, // 2. ハンドラチェーンを含むルート定義
);
SDK例 — ハンドラチェーン(推奨ブロックパターン):
ルートは applyPayloadArgs でブロックを合成し、Resultを flatMapAsync でチェーンし、lift(orThrow(...)) で終了します。これはSDKの製品ルートにある getProductRoute に一致します:
import { primitives, blocks } from '@nodeblocks/backend-sdk';
const { compose, flatMapAsync, lift, applyPayloadArgs, orThrow, withRoute } = primitives;
const {
getProductById,
normalizeImagesOfProduct,
normalizeProduct,
ProductNotFoundBlockError,
FileStorageServiceError,
} = blocks;
export const getProductRoute = withRoute({
method: 'GET',
path: '/products/:productId',
handler: compose(
applyPayloadArgs(
getProductById,
[
['context', 'db', 'products'],
['params', 'requestParams', 'productId'],
],
'product'
),
flatMapAsync(
applyPayloadArgs(
normalizeImagesOfProduct,
[
['context', 'fileStorageDriver'],
['context', 'data', 'product'],
],
'productWithNormalizedImages'
)
),
flatMapAsync(
applyPayloadArgs(
normalizeProduct,
[['context', 'data', 'productWithNormalizedImages']],
'normalizedProduct'
)
),
lift(
orThrow(
[
[ProductNotFoundBlockError, 404],
[FileStorageServiceError, 500],
],
[['context', 'data', 'normalizedProduct']]
)
)
),
});
従来ノート: いくつかのサービス(例:プロフィールCRUD)は、ブロックエラーの代わりに
NodeblocksErrorを持つResultを返すハンドラをまだ使用しています。これらのルートはapplyPayloadArgsなしでflatMapAsync(handlerFn)を使用し、しばしばorThrowに空のエラーマップを渡します。Route » 従来:ハンドラでの合成 を参照してください。
ロギング: 本番ルートはしばしばハンドラとブロックを
withLoggingでラップします(Handler Wrappers » withLogging を参照)。以下のcreateProductRouteの例ではそれが含まれています;getProductRouteのような読み取り専用ルートは省略することがあります。
利点:
- 可読性: 関数が左から右へ流れます
- 合成可能: ステップの追加/削除が容易
- テスト可能: 各関数を個別にテストできます
カリーング
カリーングは、複数の引数を取る関数を単一の引数を取る関数のシーケンスに変換する技術です。
数学的定義:
f(x, y, z) → f(x)(y)(z)
Nodeblocks(Ramda)での使用:
SDKはカリーングと部分適用にRamdaを使用します。この例は一般的なテクニックを示しています — SDKのエクスポートではありません:
import { curry } from 'ramda';
// 通常の関数
const add = (a, b) => a + b;
// カリード関数
const curriedAdd = curry(add);
const addFive = curriedAdd(5);
const result = addFive(3); // 8
SDK例 — バリデーターファクトリ:
バリデーターは (payload) => Promise<void> を返す高階関数です。isAuthenticated() や checkIdentityType(['admin']) などのファクトリ関数は、ルート定義時に部分適用されます:
import { primitives, validators } from '@nodeblocks/backend-sdk';
const { withRoute } = primitives;
const { isAuthenticated, checkIdentityType } = validators;
// isAuthenticated() はファクトリ — バリデーターを取得するために一度呼び出します
const protectedRoute = withRoute({
method: 'GET',
path: '/secret',
validators: [isAuthenticated()],
handler: secretHandler,
});
利点:
- 部分適用: 専用関数を作成
- 再利用性: 同じ関数、異なる設定
- 合成可能: 他の関数と組み合わせて簡単に使用
部分適用
部分適用は、関数のいくつかの引数を固定して、より小さいarityの別の関数を生成するプロセスです。
数学的定義:
f(x, y, z) → f(x, y, _) → g(z)
Nodeblocks(Ramda)での使用:
SDKはサービス配線にRamdaの partial を使用します。この例は一般的なテクニックを示しています:
import { partial } from 'ramda';
// 元の関数
const multiply = (a, b) => a * b;
// 最初の引数を通部分適用
const multiplyByTwo = partial(multiply, [2]);
const result = multiplyByTwo(5); // 10
SDK例 — サービス設定:
サービスは defService(partial(compose(...features), [serviceContext])) で構築されます。サービスコンテキスト — authenticate、configuration、dataStores、およびオプションのドライバー — はサービス作成時に1回だけ固定されます:
import express from 'express';
import { partial, compose } from 'ramda';
import { services, drivers, middlewares } from '@nodeblocks/backend-sdk';
const { authService } = services;
const { withMongo } = drivers;
const { nodeBlocksErrorMiddleware } = middlewares;
const connectToDatabase = withMongo(
'mongodb://localhost:27017/?authSource=admin',
'dev',
'user',
'password'
);
// authService は内部で以下を使用します:
// defService(partial(compose(registerCredentialsFeature, loginWithCredentialsFeature, ...), [{
// authenticate, configuration, dataStores, mailService, googleOAuthDriver, ...
// }]))
express()
.use(
authService(
{
...(await connectToDatabase('identities')),
...(await connectToDatabase('onetimetokens')),
},
{
authSecrets: {
authEncSecret: 'your-encryption-secret',
authSignSecret: 'your-signing-secret',
},
authMode: 'bearer',
identity: { typeIds: { admin: '100', guest: '000', regular: '001' } },
},
{
// mailService、// オプション — メール検証 / MFAフローに必要
}
)
)
.use(nodeBlocksErrorMiddleware())
.listen(8089, () => console.log('Server running'));
利点:
- 設定: デフォルトパラメータで関数をセットアップ
- 柔軟性: 同じ関数、異なる設定
- コードのクリーンさ: 反復の削減
高階関数
高階関数は、引数として関数を受け取ったり、結果として関数を返したりする関数です。
数学的定義:
H(f) = g, ただし f と g は関数
Nodeblocksでの使用:
import { primitives } from '@nodeblocks/backend-sdk';
// バリデーターファクトリ:フルルートのペイロードを受け取る関数を返します
const createEmailValidator = (errorMessage: string): primitives.Validator => {
return async ({ params }) => {
if (!params.requestBody?.email?.includes('@')) {
throw new primitives.NodeblocksError(400, errorMessage);
}
};
};
// 使用
const requireEmail = createEmailValidator('メールが必要です');
SDK例 — ルートファクトリ:
withRoute そのものも高階関数です。ルートファクトリは反復を減らすためにそれをラップします。戻り値はスタンドアロンのExpressルートではなく、defService 内で使用される RouteComposer です:
import { primitives, validators } from '@nodeblocks/backend-sdk';
const { withRoute } = primitives;
const { isAuthenticated } = validators;
const createPostRoute = (path: string, handler, validators = []) => {
return withRoute({
method: 'POST',
path,
handler,
validators,
});
};
// サービスフィーチャー合成内での使用
const createProfileRoute = createPostRoute('/profiles', createProfileHandler, [isAuthenticated()]);
🔧 Nodeblocks関数型パターン
Result型(モナド)
Nodeblocksはブロックとハンドラチェーン内でneverthrowの Result 型を使用して、成功と失敗のケースを明示的に処理します。ブロックは Promise<Result<T, SpecificBlockError>> を返します;applyPayloadArgs は成功した値を payload.context.data にマージします。
neverrolloの基本(ブロック内で使用):
import { ok, err } from 'neverthrow';
const validateEmail = (email: string) => {
if (!email) {
return err(new Error('メールが必要です'));
}
return ok(email);
};
SDKハンドラチェーンパターン:
ルート作成は、ハンドラ(書き込み)とブロック(読み取りバック + 正規化)を組み合せます。チェーンは lift(orThrow(...)) で終了し、ブロックエラーをHTTPステータスコードにマッピングします。これはSDKの製品ルートの createProductRoute に一致します:
import { primitives, blocks, handlers, validators } from '@nodeblocks/backend-sdk';
const { compose, flatMapAsync, lift, applyPayloadArgs, orThrow, withRoute, withLogging } = primitives;
const { createProduct } = handlers;
const {
getProductById,
normalizeProduct,
ProductNotFoundBlockError,
FileStorageServiceError,
} = blocks;
const { isAuthenticated, checkIdentityType } = validators;
export const createProductRoute = withRoute({
method: 'POST',
path: '/products',
validators: [isAuthenticated(), checkIdentityType(['admin'])],
handler: compose(
withLogging(createProduct),
flatMapAsync(
applyPayloadArgs(
getProductById,
[
['context', 'db', 'products'],
['context', 'data', 'productId'],
],
'product'
)
),
flatMapAsync(
applyPayloadArgs(
normalizeProduct,
[['context', 'data', 'product']],
'normalizedProduct'
)
),
lift(
orThrow(
[
[ProductNotFoundBlockError, 404],
[FileStorageServiceError, 500],
],
[['context', 'data', 'normalizedProduct']]
)
)
),
});
関数のリフト
SDKにおいて、lift はハンドラチェーンの終了ステップを適応します。それは合成された前のステップからの Promise を待機し、アンラップされた値をその関数に渡します — 通常は orThrow で、ブロックエラーをHTTPステータスコードにマッピングし、レスポンスをフォーマットします:
import { primitives } from '@nodeblocks/backend-sdk';
const { compose, flatMapAsync, lift, applyPayloadArgs, orThrow } = primitives;
// lift は orThrow をラップします — 標準的な終了パターン
handler: compose(
applyPayloadArgs(someBlock, [/* パス */], 'result'),
flatMapAsync(applyPayloadArgs(anotherBlock, [/* パス */], 'nextResult')),
lift(orThrow([[SomeBlockError, 404], [SomeBlockError, 500]], [['context', 'data', 'nextResult'], 200])),
)
lift はプレーン関数をResultファンクタにリフトしません。それは合成されたハンドラステップ間の非同期Promiseと同期終了橋渡しをします。
パイプライン合成
完全なルートハンドラパイプラインは以下のパターンに従います:ブロック/ハンドラ → flatMapAsyncチェーン → lift(orThrow) 終了。
完全なSDKルートについては上記のgetProductRoute例を参照してください。ステップ별:
- 取得 —
applyPayloadArgs(block, paths, key)はブロックを実行し、その結果をpayload.context.dataに保存 - 変換 —
flatMapAsync(applyPayloadArgs(...))は前のステップが成功した場合、追加のブロックをチェーン - 終了 —
lift(orThrow(errorMap, successMap))はブロックエラーをHTTPステータスコードにマッピングし、レスポンスデータを抽出
書き込みルート(例:createProductRoute)は最初のステップとしてハンドラを追加し、しばしば withLogging でラップされます。
🧮 数学的基礎
圏論
Nodeblocksパターンは圏論のコンセプトに触発されています:
ファンクタ(neverthrow):
import { ok } from 'neverthrow';
// Result はファンクタ — マップできます
const userResult = ok({ name: 'John', email: 'john@example.com' });
const formattedResult = userResult.map(user => ({
...user,
displayName: user.name.toUpperCase()
}));
モナド(neverthrow):
import { ok } from 'neverthrow';
// 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('加算中:', 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, blocks } from '@nodeblocks/backend-sdk';
const { compose, flatMapAsync, lift, applyPayloadArgs, orThrow } = primitives;
// ✅ 合成されたハンドラチェーン
const handler = compose(
applyPayloadArgs(someBlock, [/* パス */], 'result'),
flatMapAsync(applyPayloadArgs(nextBlock, [/* パス */], 'next')),
lift(orThrow([[BlockError, 404]], [['context', 'data', 'next'], 200])),
);
// ❌ モノリシック
const handler = async (payload) => {
// 100行の混ざった関心
};
4. Resultでのエラー処理
- ブロックは期待されるエラーに
Resultを返します;orThrowはそれをHTTPレスポンスにマッピング - バリデーターはリクエストレベルの却下に対して
NodeblocksErrorを投げる - 予期されるブロックエラーを
orThrowエラーマップにすべて含める — マップされていないエラー(例:getProductByIdのProductUnexpectedDBError)は未処理の失敗として伝播し、エラーミドルウェア経由で500レスポンスになる(エラー処理 を参照)
import { ok, err } from 'neverthrow';
// ✅ ブロック内 — 明示的なResult
const findProfile = async (db, id) => {
const profile = await db.profiles.findOne({ id });
if (!profile) {
return err(new ProfileNotFoundBlockError('Profile not found'));
}
return ok(profile);
};
// ✅ ルートレベル — orThrowがResult → HTTPマッピングを処理
lift(orThrow([[ProfileNotFoundBlockError, 404]], [['context', 'data', 'profile'], 200]))
// ❌ 同じレイヤーで throw と Result の混合