reduce関数の実践的な使い方|実務で使える業務パターン集

未分類

reduce関数の実践的な使い方|実務で使える業務パターン集

プログラミングを学ぶ過程で「reduce関数は難しい」という評判をよく聞きます。しかし実務では、reduceこそが最も効果的にデータを処理できる強力なツールです。本記事では教科書的な説明ではなく、実際の業務で日々使われるパターンを中心に解説します。

reduceの基本概念

reduceは配列のすべての要素に対して関数を実行し、単一の値に集約する関数です。

// 基本形式\nconst result = array.reduce((accumulator, currentValue, index, array) => {\n  // 処理\n  return accumulator; // 次の反復に渡す値\n}, initialValue);

TypeScriptの型定義:

reduce<U>(\n  callback: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U,\n  initialValue: U\n): U

Pythonでは関数型プログラミングの「functools.reduce」として提供されます:

from functools import reduce\nresult = reduce(lambda acc, val: acc + val, [1, 2, 3, 4], 0)

実務で頻出するユースケース

1. データの集計・合計

最も基本的かつ頻繁に使用するパターンです。単なる合計ではなく、条件付き集計が実務では多いです。

// 実務例:売上データから特定条件の合計を計算
interface SalesRecord {\n  date: string;\n  amount: number;\n  status: 'completed' | 'pending' | 'cancelled';\n  category: string;\n}\n\nconst salesData: SalesRecord[] = [\n  { date: '2024-01-15', amount: 50000, status: 'completed', category: 'A' },\n  { date: '2024-01-16', amount: 30000, status: 'pending', category: 'B' },\n  { date: '2024-01-17', amount: 80000, status: 'completed', category: 'A' },\n  { date: '2024-01-18', amount: 20000, status: 'cancelled', category: 'C' },\n  { date: '2024-01-19', amount: 60000, status: 'completed', category: 'B' },\n];\n\n// 完了済みの売上のみ集計\nconst completedSalesTotal = salesData.reduce((sum, record) => {\n  return record.status === 'completed' ? sum + record.amount : sum;\n}, 0);\n\nconsole.log(completedSalesTotal); // 190000

Pythonの同等実装:

from functools import reduce\nfrom typing import List, TypedDict\n\nclass SalesRecord(TypedDict):\n    date: str\n    amount: int\n    status: str\n    category: str\n\nsales_data: List[SalesRecord] = [\n    {'date': '2024-01-15', 'amount': 50000, 'status': 'completed', 'category': 'A'},\n    {'date': '2024-01-16', 'amount': 30000, 'status': 'pending', 'category': 'B'},\n    {'date': '2024-01-17', 'amount': 80000, 'status': 'completed', 'category': 'A'},\n    {'date': '2024-01-18', 'amount': 20000, 'status': 'cancelled', 'category': 'C'},\n    {'date': '2024-01-19', 'amount': 60000, 'status': 'completed', 'category': 'B'},\n]\n\ncompleted_total = reduce(\n    lambda acc, record: acc + record['amount'] if record['status'] == 'completed' else acc,\n    sales_data,\n    0\n)\n\nprint(completed_total)  # 190000

2. オブジェクトの構築・変換

配列からオブジェクトを生成する処理は実務で非常に多いです。データベース結果の整形、APIレスポンスの変換など様々な場面で活躍します。

// 実務例:ユーザーデータをIDをキーとしたオブジェクトマップに変換\ninterface User {\n  id: number;\n  name: string;\n  email: string;\n  department: string;\n}\n\nconst users: User[] = [\n  { id: 101, name: '田中太郎', email: 'tanaka@example.com', department: '営業' },\n  { id: 102, name: '佐藤花子', email: 'sato@example.com', department: 'マーケ' },\n  { id: 103, name: '鈴木次郎', email: 'suzuki@example.com', department: 'IT' },\n];\n\n// IDをキーとしたマップを生成\nconst userMap = users.reduce>((map, user) => {\n  map[user.id] = user;\n  return map;\n}, {});\n\nconsole.log(userMap[102]); // { id: 102, name: '佐藤花子', ... }\n\n// さらに高度な例:複数のキーでのグループ化\nconst usersByDepartment = users.reduce>((map, user) => {\n  if (!map[user.department]) {\n    map[user.department] = [];\n  }\n  map[user.department].push(user);\n  return map;\n}, {});\n\nconsole.log(usersByDepartment['IT']); // IT部門のユーザー一覧

Pythonの実装:

from functools import reduce\nfrom typing import Dict, List, TypedDict\n\nclass User(TypedDict):\n    id: int\n    name: str\n    email: str\n    department: str\n\nusers: List[User] = [\n    {'id': 101, 'name': '田中太郎', 'email': 'tanaka@example.com', 'department': '営業'},\n    {'id': 102, 'name': '佐藤花子', 'email': 'sato@example.com', 'department': 'マーケ'},\n    {'id': 103, 'name': '鈴木次郎', 'email': 'suzuki@example.com', 'department': 'IT'},\n]\n\n# IDをキーとしたマップを生成\nuser_map: Dict[int, User] = reduce(\n    lambda acc, user: {**acc, user['id']: user},\n    users,\n    {}\n)\n\nprint(user_map[102])\n\n# 部門でグループ化\nusers_by_dept: Dict[str, List[User]] = reduce(\n    lambda acc, user: {\n        **acc,\n        user['department']: acc.get(user['department'], []) + [user]\n    },\n    users,\n    {}\n)\n\nprint(users_by_dept['IT'])

3. 複雑なフィルタリングと変換の組み合わせ

実務では単なるフィルタリングではなく、条件に応じた変換が必要な場合がほとんどです。reduceなら1パスで処理できます。

// 実務例:注文データから統計情報を抽出\ninterface Order {\n  id: string;\n  customerId: number;\n  amount: number;\n  items: number;\n  createdAt: Date;\n  status: 'pending' | 'shipped' | 'delivered' | 'returned';\n}\n\nconst orders: Order[] = [\n  { id: 'ORD001', customerId: 1001, amount: 15000, items: 3, createdAt: new Date('2024-01-10'), status: 'delivered' },\n  { id: 'ORD002', customerId: 1002, amount: 8000, items: 1, createdAt: new Date('2024-01-12'), status: 'shipped' },\n  { id: 'ORD003', customerId: 1001, amount: 25000, items: 5, createdAt: new Date('2024-01-15'), status: 'delivered' },\n  { id: 'ORD004', customerId: 1003, amount: 5000, items: 1, createdAt: new Date('2024-01-18'), status: 'pending' },\n  { id: 'ORD005', customerId: 1002, amount: 12000, items: 2, createdAt: new Date('2024-01-20'), status: 'returned' },\n];\n\ninterface OrderStats {\n  totalRevenue: number;\n  deliveredCount: number;\n  averageOrderValue: number;\n  totalItems: number;\n  returnsCount: number;\n  topCustomerId?: number;\n  topCustomerSpent?: number;\n}\n\n// 1パスで複数の統計情報を計算\nconst stats = orders.reduce(\n  (acc, order) => {\n    const isDelivered = order.status === 'delivered';\n    const isReturned = order.status === 'returned';\n    const isCompleted = isDelivered || order.status === 'shipped';\n\n    return {\n      totalRevenue: isCompleted ? acc.totalRevenue + order.amount : acc.totalRevenue,\n      deliveredCount: isDelivered ? acc.deliveredCount + 1 : acc.deliveredCount,\n      totalItems: acc.totalItems + (isCompleted ? order.items : 0),\n      averageOrderValue: 0, // 後で計算\n      returnsCount: isReturned ? acc.returnsCount + 1 : acc.returnsCount,\n      topCustomerId: acc.topCustomerSpent === undefined || order.amount > acc.topCustomerSpent\n        ? order.customerId\n        : acc.topCustomerId,\n      topCustomerSpent: acc.topCustomerSpent === undefined || order.amount > acc.topCustomerSpent\n        ? order.amount\n        : acc.topCustomerSpent,\n    };\n  },\n  {\n    totalRevenue: 0,\n    deliveredCount: 0,\n    totalItems: 0,\n    averageOrderValue: 0,\n    returnsCount: 0,\n  }\n);\n\n// 平均値を計算\nstats.averageOrderValue = stats.deliveredCount > 0\n  ? Math.round(stats.totalRevenue / stats.deliveredCount)\n  : 0;\n\nconsole.log(stats);

4. 状態管理と累積処理

特に複雑なビジネスロジックの実装では、reduceが状態管理に最適です。

// 実務例:イベントログから最終状態を計算\ninterface StateEvent {\n  timestamp: Date;\n  type: 'created' | 'updated' | 'approved' | 'rejected' | 'published';\n  changes?: Record;\n}\n\ninterface DocumentState {\n  status: 'draft' | 'pending_review' | 'approved' | 'rejected' | 'published';\n  lastUpdated: Date;\n  approvalCount: number;\n  rejectionCount: number;\n  metadata: Record;\n}\n\nconst events: StateEvent[] = [\n  { timestamp: new Date('2024-01-10T10:00:00'), type: 'created', changes: { title: 'Report Q1' } },\n  { timestamp: new Date('2024-01-12T14:30:00'), type: 'updated', changes: { content: 'Updated content' } },\n  { timestamp: new Date('2024-01-15T09:00:00'), type: 'approved', changes: { approver: 'manager_a' } },\n  { timestamp: new Date('2024-01-15T15:00:00'), type: 'approved', changes: { approver: 'manager_b' } },\n  { timestamp: new Date('2024-01-18T11:00:00'), type: 'published', changes: {} },\n];\n\nconst finalState = events.reduce(\n  (state, event) => {\n    switch (event.type) {\n      case 'created':\n        return {\n          ...state,\n          status: 'draft',\n          lastUpdated: event.timestamp,\n          metadata: event.changes || {},\n        };\n      case 'updated':\n        return {\n          ...state,\n          lastUpdated: event.timestamp,\n          metadata: { ...state.metadata, ...event.changes },\n        };\n      case 'approved':\n        return {\n          ...state,\n          status: state.approvalCount + 1 >= 2 ? 'approved' : 'pending_review',\n          approvalCount: state.approvalCount + 1,\n          lastUpdated: event.timestamp,\n        };\n      case 'rejected':\n        return {\n          ...state,\n          status: 'rejected',\n          rejectionCount: state.rejectionCount + 1,\n          lastUpdated: event.timestamp,\n        };\n      case 'published':\n        return {\n          ...state,\n          status: 'published',\n          lastUpdated: event.timestamp,\n        };\n      default:\n        return state;\n    }\n  },\n  {\n    status: 'draft',\n    lastUpdated: new Date(),\n    approvalCount: 0,\n    rejectionCount: 0,\n    metadata: {},\n  }\n);\n\nconsole.log(finalState); // 最終状態が得られる

高度な応用パターン

パターン1:チェーン可能な変換(map + filter相当)

// 複数の変換をチェーンする\ninterface Product {\n  id: number;\n  name: string;\n  price: number;\n  stock: number;\n  category: string;\n}\n\nconst products: Product[] = [\n  { id: 1, name: 'ノートPC', price: 150000, stock: 5, category: '電子機器' },\n  { id: 2, name: 'マウス', price: 3000, stock: 50, category '電子機器' },\n  { id: 3, name: '机', price: 45000, stock: 0, category: '家具' },\n  { id: 4, name: 'キーボード', price: 12000, stock: 20, category: '電子機器' },\n];\n\n// 在庫がある商品のみを取得し、税込価格に変換\nconst inStockWithTax = products.reduce>(\n  (acc, product) => {\n    if (product.stock > 0) {\n      acc.push({\n        id: product.id,\n        name: product.name,\n        priceWithTax: Math.round(product.price * 1.1),\n      });\n    }\n    return acc;\n  },\n  []\n);

パターン2:ネストされたデータ構造の操作

// 実務例:階層化されたメニュー構造の生成\ninterface MenuSource {\n  id: number;\n  label: string;\n  parentId: number | null;\n  order: number;\n}\n\ninterface MenuNode {\n  id: number;\n  label: string;\n  order: number;\n  children: MenuNode[];\n}\n\nconst menuItems: MenuSource[] = [\n  { id: 1, label: 'ホーム', parentId: null, order: 1 },\n  { id: 2, label: 'サービス', parentId: null, order: 2 },\n  { id: 3, label: 'Web制作', parentId: 2, order: 1 },\n  { id: 4, label: 'アプリ開発', parentId: 2, order: 2 },\n  { id: 5, label: 'お問い合わせ', parentId: null, order: 3 },\n  { id: 6, label: 'コンサルティング', parentId: 2, order: 3 },\n];\n\nconst buildMenuTree = (items: MenuSource[]): MenuNode[] => {\n  // まず全アイテムをMapに変換\n  const itemMap = items.reduce>(\n    (map, item) => {\n      map.set(item.id, {\n        id: item.id,\n        label: item.label,\n        order: item.order,\n        children: [],\n      });\n      return map;\n    },\n    new Map()\n  );\n\n  // 親子関係を構築\n  const roots: MenuNode[] = [];\n  items.forEach(item => {\n    const node = itemMap.get(item.id)!;\n    if (item.parentId === null) {\n      roots.push(node);\n    } else {\n      const parent = itemMap.get(item.parentId);\n      if (parent) {\n        parent.children.push(node);\n      }\n    }\n  });\n\n  return roots.sort((a, b) => a.order - b.order);\n};\n\nconst menuTree = buildMenuTree(menuItems);

パターン3:条件分岐による複数結果の同時計算

// 実務例:レポート生成時に複数の統計を同時計算\ninterface Transaction {\n  id: string;\n  amount: number;\n  type: 'income' | 'expense';\n  category: string;\n  date: Date;\n}\n\ninterface FinancialReport {\n  totalIncome: number;\n  totalExpense: number;\n  netProfit: number;\n  incomeByCategory: Record;\n  expenseByCategory: Record;\n  monthlyTotals: Record;\n}\n\nconst transactions: Transaction[] = [\n  { id: 'TXN001', amount: 500000, type: 'income', category: 'sales', date: new Date('2024-01-05') },\n  { id: 'TXN002', amount: 120000, type: 'expense', category: 'salary', date: new Date('2024-01-10') },\n  { id: 'TXN003', amount: 800000, type: 'income', category: 'services', date: new Date('2024-01-15') },\n  { id: 'TXN004', amount: 50000, type: 'expense', category: 'utilities', date: new Date('2024-01-20') },\n  { id: 'TXN005', amount: 100000, type: 'expense', category: 'marketing', date: new Date('2024-02-05') },\n];\n\nconst generateReport = (txns: Transaction[]): FinancialReport => {\n  return txns.reduce(\n    (report, txn) => {\n      const monthKey = txn.date.toISOString().slice(0, 7); // 'YYYY-MM'\n\n      if (txn.type === 'income') {\n        return {\n          ...report,\n          totalIncome: report.totalIncome + txn.amount,\n          netProfit: report.netProfit + txn.amount,\n          incomeByCategory: {\n            ...report.incomeByCategory,\n            [txn.category]: (report.incomeByCategory[txn.category] || 0) + txn.amount,\n          },\n          monthlyTotals: {\n            ...report.monthlyTotals,\n            [monthKey]: {\n              income: (report.monthlyTotals[monthKey]?.income || 0) + txn.amount,\n              expense: report.monthlyTotals[monthKey]?.expense || 0,\n            },\n          },\n        };\n      } else {\n        return {\n          ...report,\n          totalExpense: report.totalExpense + txn.amount,\n          netProfit: report.netProfit - txn.amount,\n          expenseByCategory: {\n            ...report.expenseByCategory,\n            [txn.category]: (report.expenseByCategory[txn.category] || 0) + txn.amount,\n          },\n          monthlyTotals: {\n            ...report.monthlyTotals,\n            [monthKey]: {\n              income: report.monthlyTotals[monthKey]?.income || 0,\n              expense: (report.monthlyTotals[monthKey]?.expense || 0) + txn.amount,\n            },\n          },\n        };\n      }\n    },\n    {\n      totalIncome: 0,\n      totalExpense: 0,\n      netProfit: 0,\n      incomeByCategory: {},\n      expenseByCategory: {},\n      monthlyTotals: {},\n    }\n  );\n};\n\nconst report = generateReport(transactions);

Pythonでの実践例

from functools import reduce\nfrom typing import Dict, List, TypedDict\nfrom datetime import datetime\n\nclass Transaction(TypedDict):\n    id: str\n    amount: int\n    type: str  # 'income' or 'expense'\n    category: str\n    date: datetime\n\nclass MonthlyTotal(TypedDict):\n    income: int\n    expense: int\n\nclass FinancialReport(TypedDict):\n    total_income: int\n    total_expense: int\n    net_profit: int\n    income_by_category: Dict[str, int]\n    expense_by_category: Dict[str, int]\n    monthly_totals: Dict[str, MonthlyTotal]\n\ndef process_transaction(report: FinancialReport, txn: Transaction) -> FinancialReport:\n    month_key = txn['date'].strftime('%Y-%m')\n    \n    if txn['type'] == 'income':\n        income_cat = report['income_by_category']\n        income_cat[txn['category']] = income_cat.get(txn['category'], 0) + txn['amount']\n        \n        if month_key not in report['monthly_totals']:\n            report['monthly_totals'][month_key] = {'income': 0, 'expense': 0}\n        \n        report['monthly_totals'][month_key]['income'] += txn['amount']\n        report['total_income'] += txn['amount']\n        report['net_profit'] += txn['amount']\n    else:\n        expense_cat = report['expense_by_category']\n        expense_cat[txn['category']] = expense_cat.get(txn['category'], 0) + txn['amount']\n        \n        if month_key not in report['monthly_totals']:\n            report['monthly_totals'][month_key] = {'income': 0, 'expense': 0}\n        \n        report['monthly_totals'][month_key]['expense'] += txn['amount']\n        report['total_expense'] += txn['amount']\n        report['net_profit'] -= txn['amount']\n    \n    return report\n\ntransactions: List[Transaction] = [\n    {'id': 'TXN001', 'amount': 500000, 'type': 'income', 'category': 'sales', 'date': datetime(2024, 1, 5)},\n    {'id': 'TXN002', 'amount': 120000, 'type': 'expense', 'category': 'salary', 'date': datetime(2024, 1, 10)},\n    {'id': 'TXN003', 'amount': 800000, 'type': 'income', 'category': 'services', 'date': datetime(2024, 1, 15)},\n]\n\ninitial_report: FinancialReport = {\n    'total_income': 0,\n    'total_expense': 0,\n    'net_profit': 0,\n    'income_by_category': {},\n    'expense_by_category': {},\n    'monthly_totals': {},\n}\n\nreport = reduce(process_transaction, transactions, initial_report)\nprint(report)

実装時の注意点と落とし穴

1. イミュータビリティの重要性

reduceで最も多い罪は元のaccumulatorを直接変更してしまうことです。これはバグの原因になります。

// ❌ 悪い例:直接変更
const badResult = data.reduce((acc, item) => {\n  acc[item.id] = item; // オブジェクトを直接変更\n  return acc;\n}, {});\n\n// ✅ 良い例:新しいオブジェクトを返す\nconst goodResult = data.reduce((acc, item) => ({\n  ...acc,\n  [item.id]: item, // スプレッド演算子で新規作成\n}), {});\n\n// ✅ 配列の場合\nconst arrayResult = data.reduce((acc, item) => [\n  ...acc,\n  item,\n], []);

2. パフォーマンスの考慮

複雑な変換や大規模データでは、reduceの効率性に注意が必要です。

// 10,000件のデータで複雑な処理\nconst largeData = Array.from({ length: 10000 }, (_, i) => ({\n  id: i,\n  value: Math.random() * 1000,\n}));\n\n// 注意:毎回スプレッド演算子でオブジェクトをコピーすると遅い\n// const bad = largeData.reduce((acc, item) => ({...acc, [item.id]: item}), {});\n\n// 改善:Object.assignを使用\nconst better = largeData.reduce((acc, item) => {\n  acc[item.id] = item;\n  return acc;\n}, {} as Record);\n\n// さらに改善:Mapを使用(キーの多い場合)\nconst best = largeData.reduce((acc, item) => {\n  acc.set(item.id, item);\n  return acc;\n}, new Map());

3. 初期値の設定ミス

初期値を設定しないと予期しない型エラーが発生します。

// ❌ エラーになりやすい\nconst result = [1, 2, 3].reduce((acc, val) => acc + val);\n// 初回: acc = 1, val = 2\n// 意図と異なる可能性がある\n\n// ✅ 明確に初期値を指定\nconst correctResult = [1, 2, 3].reduce((acc, val) => acc + val, 0);\n// 初回: acc = 0, val = 1\n\n// 空配列対策\nconst safeResult = emptyArray.reduce((acc, val) => acc + val, 0); // 0を返す

4. 複雑さのバランス

reduceが便利だからといって無理に詰め込むと、可読性が低下します。


      
タイトルとURLをコピーしました