メインコンテンツまでスキップ
バージョン: 0.4.2

🧮 関数型プログラミング概念

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)))

利点:

  • 読みやすい: 関数が左から右に流れる
  • 構成可能: ステップの追加/削除が簡単
  • テスト可能: 各関数を独立してテスト可能

カリー化

カリー化は、複数の引数を取る関数を、それぞれが単一の引数を取る関数のシーケンスに変換する技術です。

数学的定義:

f(x, y, z) → f(x)(y)(z)

Nodeblocksでは:

import { curry } from 'ramda';

// 通常の関数
const add = (a, b) => a + b;

// カリー化された関数
const curriedAdd = curry(add);

// 使用方法
const add5 = curriedAdd(5); // 部分適用
const result = add5(3); // 8

// またはまとめて
const result = curriedAdd(5)(3); // 8

実用的な例:

// ログレベル付きログ関数
const logWithLevel = curry((level, message) => {
console.log(`[${level}] ${message}`);
});

// 特化ログ関数を作成
const logError = logWithLevel('ERROR');
const logInfo = logWithLevel('INFO');

// 使用
logError('データベース接続に失敗しました');
logInfo('ユーザーが正常に作成されました');

部分適用

部分適用は、関数の一部の引数を固定して新しい関数を作成する技術です。

import { partial } from 'lodash';

// データベース操作関数
const findUsers = (db, filter, options) => {
return db.users.find(filter, options);
};

// 特定のデータベースで部分適用
const findUsersInMyDB = partial(findUsers, myDatabase);

// 使用
const activeUsers = findUsersInMyDB({ status: 'active' }, { limit: 10 });
const adminUsers = findUsersInMyDB({ role: 'admin' }, { sort: { name: 1 } });

高階関数

高階関数は、他の関数を引数として受け取るか、関数を戻り値として返す関数です。

// ログ付きのハンドラーをラップする高階関数
const withLogging = (handler) => {
return (payload) => {
console.log('ハンドラー開始:', handler.name);
const result = handler(payload);
console.log('ハンドラー完了:', handler.name);
return result;
};
};

// 使用
const loggedCreateUser = withLogging(createUserHandler);
const loggedUpdateUser = withLogging(updateUserHandler);

🔧 Result型によるエラーハンドリング

Result型とは

Result型は、成功または失敗を明示的に表現する型です。例外をスローする代わりに、操作の結果を明示的に処理します。

import { ok, err, Result } from 'neverthrow';

// 成功の場合
const successResult: Result<string, Error> = ok('成功メッセージ');

// エラーの場合
const errorResult: Result<string, Error> = err(new Error('何かが間違っています'));

Result型を使用したハンドラー

import { ok, err } from 'neverthrow';
import { NodeblocksError } from '@nodeblocks/backend-sdk';

const createUser = async (userData): Promise<Result<User, NodeblocksError>> => {
// 検証
if (!userData.email) {
return err(new NodeblocksError(400, 'メールアドレスが必要です'));
}

try {
const user = await database.users.create(userData);
return ok(user);
} catch (error) {
return err(new NodeblocksError(500, 'ユーザーの作成に失敗しました'));
}
};

Result型のコンポジション

const createUserPipeline = compose(
validateUserInput, // Result<UserData, Error>を返す
flatMapAsync(createUser), // 前のResultが成功の場合のみ実行
flatMapAsync(sendWelcomeEmail), // 前のResultが成功の場合のみ実行
lift(formatUserResponse) // 最終レスポンスをフォーマット
);

🎯 圏論の基礎

ファンクター

ファンクターは、値をラップし、その値に関数を適用する方法を提供する構造です。

// Result型はファンクター
const result = ok(42);

// map を使用して値を変換
const doubled = result.map(x => x * 2); // ok(84)

// エラーの場合、mapは適用されない
const errorResult = err(new Error('エラー'));
const stillError = errorResult.map(x => x * 2); // まだエラー

モナド

モナドは、ファンクターの拡張で、平坦化操作(flatMap)を提供します。

// チェーン操作
const userCreationPipeline = (userData) => {
return validateUser(userData)
.flatMap(validData => createUser(validData))
.flatMap(user => assignRole(user))
.flatMap(user => sendNotification(user));
};

🔄 実用的なパターン

パイプライン処理

// データ処理パイプライン
const processUserData = compose(
validateInput, // 入力検証
enrichWithDefaults, // デフォルト値で拡張
sanitizeData, // データをサニタイズ
transformFormat, // フォーマット変換
saveToDatabase // データベースに保存
);

// 使用
const result = processUserData(rawUserData);

エラー処理パイプライン

const robustUserCreation = compose(
validateUserData,
flatMapAsync(checkEmailUniqueness),
flatMapAsync(hashPassword),
flatMapAsync(saveUser),
flatMapAsync(createUserProfile),
flatMapAsync(sendWelcomeEmail),
lift(formatSuccessResponse)
);

// エラーはチェーン全体で自動的に伝播

条件付き実行

const conditionalProcessing = (shouldProcess) => compose(
validateInput,
shouldProcess ? processData : identity, // 条件付きステップ
formatOutput
);

🎯 ベストプラクティス

1. 純粋関数の使用

// 良い - 純粋関数
const calculateTotal = (items) => {
return items.reduce((sum, item) => sum + item.price, 0);
};

// 悪い - 副作用あり
let globalTotal = 0;
const calculateTotal = (items) => {
globalTotal = items.reduce((sum, item) => sum + item.price, 0);
return globalTotal;
};

2. 不変性の維持

// 良い - 不変操作
const addItem = (list, item) => [...list, item];

// 悪い - 元の配列を変更
const addItem = (list, item) => {
list.push(item);
return list;
};

3. 小さく構成可能な関数

// 良い - 小さな構成可能な関数
const validateEmail = (email) => email.includes('@');
const validateLength = (min) => (str) => str.length >= min;
const validateUser = compose(
validateEmail,
validateLength(3)
);

// 悪い - 大きなモノリシック関数
const validateUser = (user) => {
if (!user.email.includes('@')) return false;
if (user.name.length < 3) return false;
// さらに多くの検証...
};

🔗 他の概念との関連


➡️ 次へ

エラーハンドリングについて学習して、関数型プログラミングがどのように堅牢なエラー処理を可能にするかを理解しましょう!