メインコンテンツへスキップ
中級24分で読める

関数型プログラミングの基礎

関数型プログラミングの基本概念を、実例を交えて分かりやすく解説。純粋関数、イミュータビリティ、高階関数など、現代的な開発に必要な考え方を学びます。

システム開発関数型プログラミングプログラミング設計JavaScriptTypeScript

🎯 この記事で学べること

  • 1
    関数型プログラミングの基本概念と利点を理解できます
  • 2
    純粋関数と副作用の概念を把握し、実装できるようになります
  • 3
    イミュータビリティの重要性と実践方法を学べます
  • 4
    高階関数を使った柔軟なコード設計ができるようになります
  • 5
    関数合成やカリー化などの高度なテクニックを習得できます

読了時間: 約5

バグが消えた日

2015年秋、Netflixのエンジニアリングチームは危機に直面していた。

毎日数億回のストリーミングリクエストを処理するシステムで、原因不明のバグが頻発していた。ある時は動画が途中で止まり、ある時は課金が二重になり、ある時はレコメンドが狂った。バグの再現は困難で、ログを追っても原因は掴めなかった。

調査の結果、判明した事実は衝撃的だった。問題の9割が「状態の変更」に起因していた。あるスレッドがデータを書き換えている最中に、別のスレッドがそれを読む。タイミング次第で結果が変わる。まるでロシアンルーレットだった。

そこでチームは大胆な決断をした。「状態を変更しない」プログラミングへの転換。関数型プログラミングの採用だった。

半年後、バグレポートは80%減少した。デプロイの頻度は3倍になった。そして最も驚くべきことに、新人エンジニアの立ち上がりが2倍速くなった。

この成功体験が、Netflixを関数型プログラミングの積極的採用企業へと変えた。そして今、ReactやReduxといった関数型の思想を取り入れたライブラリが、Web開発の主流となっている。

数学者が作った純粋な世界

1930年代、プリンストン高等研究所。

アロンゾ・チャーチという数学者が、奇妙な記号を黒板に書いていた。λ(ラムダ)。この一文字が、後にコンピュータサイエンスの歴史を変えることになるとは、誰も想像していなかった。

チャーチが探求していたのは「計算とは何か」という根本的な問いだった。彼の答えはシンプルだった。「計算とは、関数の適用である」。

λx.x + 1

これは「xを受け取って、x + 1を返す関数」を表す。入力が同じなら、出力も同じ。外部の状態に依存しない。まるで数学の関数のように純粋だ。

当時、この理論は純粋に数学的な興味から生まれた。しかし1958年、ジョン・マッカーシーがLISPを作った時、ラムダ計算は初めてプログラミング言語として実装された。

そして60年後の今、JavaScriptのアロー関数 x => x + 1 を私たちは日常的に使っている。チャーチの純粋な数学が、毎日数十億回実行されるコードになった。

関数型プログラミングの「関数」は、数学の関数と同じです。f(x) = x + 1 のように、入力に対して必ず同じ出力を返します。これが「純粋」という意味です。

なぜ月曜日のコーヒーは火曜日と違うのか

月曜日の朝、あなたはコーヒーメーカーのボタンを押す。

// 手続き型の世界
let coffeeBeans = 100;  // グラム
let water = 500;        // ミリリットル

function makeCoffee() {
  coffeeBeans -= 20;   // 豆を消費
  water -= 200;         // 水を消費
  console.log(`コーヒーができました。残り:豆${coffeeBeans}g、水${water}ml`);
}

makeCoffee(); // 月曜日
makeCoffee(); // 火曜日
makeCoffee(); // 水曜日

水曜日、コーヒーメーカーは同じように動作するが、結果は月曜日とは違う。なぜなら、世界の状態が変わっているからだ。

これがプログラミングでも起きる。関数を呼ぶたびに結果が変わる。テストが通ったり通らなかったり。本番環境でだけ起きるバグ。すべては「変化する状態」が原因だ。

関数型プログラミングの答えはシンプルだ。「状態を変えなければいい」。

// 関数型の世界
function makeCoffee(beans, water) {
  return {
    coffee: "美味しいコーヒー",
    remaining: {
      beans: beans - 20,
      water: water - 200
    }
  };
}

const monday = makeCoffee(100, 500);
const tuesday = makeCoffee(monday.remaining.beans, monday.remaining.water);
const wednesday = makeCoffee(tuesday.remaining.beans, tuesday.remaining.water);

月曜日のコーヒーは永遠に月曜日のコーヒーだ。履歴が残り、いつでも過去に戻れる。これが「イミュータビリティ」の力だ。

Facebookが10億人を不変にした理由

2013年、FacebookはReactを発表した。その中核にあったのは、驚くべき思想だった。

「UIを変更するな。作り直せ」

従来のWeb開発では、DOMを直接操作していた。ボタンがクリックされたら、その要素を見つけて、属性を変更して、スタイルを更新して...。まるで動いている車のタイヤを交換するようなものだった。

Reactの答えは革命的だった。

// 従来の方法
document.getElementById('counter').innerText = count + 1;
document.getElementById('counter').style.color = 'red';

// Reactの方法
function Counter({ count }) {
  return <div style={{ color: count > 10 ? 'red' : 'black' }}>{count}</div>;
}

UIは関数だ。状態を受け取り、見た目を返す。状態が変われば、UIをまるごと作り直す。効率が悪そうに見える?そこで仮想DOMの出番だ。差分だけを計算して更新する。

この「不変性」の思想が、Facebookのような巨大なアプリケーションを可能にした。10億人のユーザー、数百人の開発者、数百万行のコード。それでも予測可能で、テスト可能で、理解可能。

なぜか?UIが純粋関数だからだ。同じpropsなら、同じUIが描画される。月曜日も火曜日も、東京でもニューヨークでも。

高階関数:関数を操る魔術師

プログラミングの世界には階級がある。

一般市民は「値」だ。数値、文字列、真偽値。彼らは変数に格納され、引数として渡され、結果として返される。

そして貴族が「関数」だ。彼らは値を操作する特権を持つ。しかし、JavaScript では革命が起きた。関数も市民権を得たのだ。

// 関数も値として扱える
const greet = function(name) {
  return `Hello, ${name}!`;
};

const functions = [greet, console.log, alert];
const selectedFunction = functions[0];
selectedFunction("World"); // "Hello, World!"

さらに、関数は関数を支配できる。これが「高階関数」だ。

// 関数を返す関数
function createMultiplier(factor) {
  return function(number) {
    return number * factor;
  };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5));   // 10
console.log(triple(5));   // 15

この力により、プログラマーは魔術師になる。関数を組み合わせ、変形し、新しい関数を生み出す。まるでレゴブロックのように。

map、filter、reduce:三種の神器

昔、プログラマーはforループの奴隷だった。

// 石器時代のコード
const numbers = [1, 2, 3, 4, 5];
const doubled = [];
for (let i = 0; i < numbers.length; i++) {
  doubled.push(numbers[i] * 2);
}

インデックス、境界チェック、一時変数。まるで手動で車を組み立てるような作業だった。

そこに三種の神器が現れた。

map - 変換の術

const doubled = numbers.map(x => x * 2);
// [2, 4, 6, 8, 10]

filter - 選別の術

const evens = numbers.filter(x => x % 2 === 0);
// [2, 4]

reduce - 集約の術

const sum = numbers.reduce((acc, x) => acc + x, 0);
// 15

これらは単なる便利関数ではない。思考の道具だ。「どうやって」ではなく「何を」したいかを表現する。

実際のコードで、その威力を見てみよう。

// 売上データから、高額商品の売上合計を計算
const sales = [
  { product: "iPhone", price: 999, quantity: 2 },
  { product: "iPad", price: 799, quantity: 1 },
  { product: "AirPods", price: 199, quantity: 5 },
  { product: "MacBook", price: 2399, quantity: 1 }
];

const highValueTotal = sales
  .filter(sale => sale.price > 500)           // 高額商品だけ
  .map(sale => sale.price * sale.quantity)    // 売上金額に変換
  .reduce((total, amount) => total + amount, 0); // 合計

console.log(highValueTotal); // 5196

forループなら20行。関数型なら3行。しかも意図が明確だ。

カリー化:スパイスの効いた関数

インドカレーの店に入ると、スパイスの香りに圧倒される。

クミン、コリアンダー、ターメリック。それぞれ単体でも使えるが、組み合わせると魔法が起きる。プログラミングにも、同じ魔法がある。

// 普通の関数
function addTax(price, taxRate) {
  return price * (1 + taxRate);
}

addTax(100, 0.08); // 108

これをカリー化すると:

// カリー化された関数
const addTax = taxRate => price => price * (1 + taxRate);

// 消費税8%の計算機を作る
const addJapanTax = addTax(0.08);

addJapanTax(100);  // 108
addJapanTax(200);  // 216

一つの関数から、特定の税率に特化した関数を作り出した。まるでカレーのベースからチキンカレーやシーフードカレーを作るように。

実用的な例を見てみよう。

// ログ記録システム
const log = level => timestamp => message => {
  console.log(`[${timestamp}] ${level}: ${message}`);
};

// 特定用途のロガーを作成
const errorLog = log("ERROR");
const infoLog = log("INFO");

// 現在時刻のロガー
const now = new Date().toISOString();
const errorNow = errorLog(now);
const infoNow = infoLog(now);

// 使用
errorNow("Database connection failed");
infoNow("Server started successfully");

カリー化により、汎用的な関数から特殊な関数を導出できる。設定を「焼き込んだ」関数を作れるのだ。

パイプラインと合成:UNIXの哲学

1970年代、ベル研究所でUNIXが生まれた。その設計哲学は革命的だった。

「一つのことをうまくやる小さなプログラムを作れ。そして、それらを組み合わせろ」

# UNIXパイプライン
cat data.txt | grep "error" | sort | uniq | wc -l

50年後、この哲学は関数型プログラミングで花開いた。

// 関数合成
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);

// 小さな関数たち
const trim = str => str.trim();
const toLowerCase = str => str.toLowerCase();
const removeSpaces = str => str.replace(/\s/g, '');
const reverse = str => str.split('').reverse().join('');

// パイプラインで組み合わせる
const processString = pipe(
  trim,
  toLowerCase,
  removeSpaces,
  reverse
);

processString("  Hello World  "); // "dlrowolleh"

実務での応用例:

// ユーザー登録処理のパイプライン
const validateEmail = user => {
  if (!user.email.includes('@')) {
    throw new Error('Invalid email');
  }
  return user;
};

const normalizeData = user => ({
  ...user,
  email: user.email.toLowerCase(),
  name: user.name.trim()
});

const hashPassword = user => ({
  ...user,
  password: bcrypt.hash(user.password)
});

const addTimestamp = user => ({
  ...user,
  createdAt: new Date()
});

const processNewUser = pipe(
  validateEmail,
  normalizeData,
  hashPassword,
  addTimestamp
);

// 使用
try {
  const newUser = processNewUser({
    name: "  John Doe  ",
    email: "JOHN@EXAMPLE.COM",
    password: "secret123"
  });
  // 保存処理へ
} catch (error) {
  // エラーハンドリング
}

各関数は独立してテスト可能。組み合わせは自由。まさにUNIXの理想が実現されている。

SpotifyとNetflixが選んだ道

2016年、Spotifyは大きな決断を下した。

1億人のユーザー、毎秒数万のリクエスト、そして頻繁に変わるビジネス要求。従来のオブジェクト指向アーキテクチャは限界に達していた。

彼らが選んだのは、関数型とマイクロサービスの融合だった。

// Spotifyのレコメンドエンジン(簡略化)
const getUserListeningHistory = userId => 
  fetch(`/api/users/${userId}/history`);

const extractArtists = history => 
  history.map(track => track.artist);

const findSimilarArtists = artists => 
  Promise.all(artists.map(artist => 
    fetch(`/api/artists/${artist}/similar`)
  ));

const rankByPopularity = artists => 
  artists.sort((a, b) => b.popularity - a.popularity);

const generateRecommendations = pipe(
  getUserListeningHistory,
  extractArtists,
  findSimilarArtists,
  rankByPopularity
);

各関数は独立したマイクロサービス。スケールも交換も自由。そして最も重要なのは、新人エンジニアでも理解できることだ。

Netflixも似た道を選んだ。彼らの「Falcor」ライブラリは、データを不変の値として扱う。変更ではなく、新しいバージョンを作る。

結果?両社とも99.95%以上の可用性を実現している。関数型プログラミングは、スケールの問題も解決したのだ。

エラーは値である

従来のプログラミングでは、エラーは爆弾だった。

// 危険なコード
function divide(a, b) {
  if (b === 0) {
    throw new Error("Division by zero!");
  }
  return a / b;
}

try {
  const result = divide(10, 0);
} catch (error) {
  console.error(error);
}

throwは関数の純粋性を破壊する。いつ爆発するか分からない爆弾を抱えているようなものだ。

関数型の答えは美しい。「エラーも値として返せばいい」。

// Result型の実装
class Ok {
  constructor(value) {
    this.value = value;
  }
  map(fn) {
    return new Ok(fn(this.value));
  }
  isOk() {
    return true;
  }
}

class Err {
  constructor(error) {
    this.error = error;
  }
  map(fn) {
    return this; // エラーはそのまま伝播
  }
  isOk() {
    return false;
  }
}

// 安全な除算
function safeDivide(a, b) {
  if (b === 0) {
    return new Err("Division by zero");
  }
  return new Ok(a / b);
}

// 使用
const result = safeDivide(10, 2)
  .map(x => x * 2)
  .map(x => x + 1);

if (result.isOk()) {
  console.log(result.value); // 11
} else {
  console.log(result.error);
}

エラーは普通の値となり、普通に扱える。爆弾は解除された。

副作用という名の現実

純粋な関数の世界は美しい。しかし、現実世界は副作用だらけだ。

ファイルの読み書き、ネットワーク通信、画面への出力。これらなしにプログラムは意味を持たない。関数型プログラミングは現実逃避なのか?

いいえ、違う。関数型は副作用を「隔離」する。

// 副作用を境界に追いやる
const app = {
  // 純粋な核心部分
  pure: {
    calculatePrice: (items, taxRate) => 
      items.reduce((sum, item) => sum + item.price, 0) * (1 + taxRate),
    
    formatReceipt: (items, total) => 
      `Items: ${items.length}\nTotal: $${total.toFixed(2)}`,
    
    validateCoupon: (coupon, currentDate) => 
      coupon.expiryDate > currentDate
  },
  
  // 副作用は境界に
  effects: {
    readItems: () => 
      fetch('/api/items').then(r => r.json()),
    
    printReceipt: (receipt) => 
      console.log(receipt),
    
    saveOrder: (order) => 
      fetch('/api/orders', { method: 'POST', body: JSON.stringify(order) })
  }
};

// メイン処理
async function processOrder() {
  // 副作用
  const items = await app.effects.readItems();
  
  // 純粋な処理
  const total = app.pure.calculatePrice(items, 0.08);
  const receipt = app.pure.formatReceipt(items, total);
  
  // 副作用
  app.effects.printReceipt(receipt);
  await app.effects.saveOrder({ items, total });
}

核心部分は純粋で、テスト可能で、理解しやすい。副作用は薄い層として境界に存在する。これが関数型アーキテクチャの真髄だ。

完全に純粋なプログラムは、哲学的には美しいが実用的ではありません。大切なのは、純粋な部分と副作用を持つ部分を明確に分離することです。

関数型の未来

2024年、関数型プログラミングは特殊な技法ではなくなった。

React Hooks は関数型の思想そのもの。TypeScriptは高度な型システムで関数型を支援。RustやGoも関数型の要素を取り入れている。

なぜ関数型は広まったのか?答えは3つある。

1. 複雑さの管理 システムが巨大化する中、状態の管理は悪夢となった。不変性と純粋関数は、この複雑さを飼いならす。

2. 並行処理の時代 マルチコアCPU、分散システム、クラウド。並行処理は避けられない。純粋関数は本質的にスレッドセーフだ。

3. AI/MLとの親和性 機械学習は本質的に関数的だ。入力を変換して出力を得る。TensorFlowやPyTorchも関数型の影響を受けている。

年代出来事影響
1930年代ラムダ計算理論的基礎
1958年LISP最初の実装
1990年代Haskell純粋関数型言語
2000年代MapReduceビッグデータ処理
2010年代React/Reduxフロントエンドの革命
2020年代主流化あらゆる言語に浸透

始めるための第一歩

関数型プログラミングは山登りのようなものだ。

頂上には純粋関数型言語Haskellがそびえ立つ。しかし、いきなり登る必要はない。まずは丘から始めよう。

// Step 1: forループをmapに
// Before
const doubled = [];
for (let i = 0; i < numbers.length; i++) {
  doubled.push(numbers[i] * 2);
}

// After
const doubled = numbers.map(n => n * 2);

// Step 2: 変更を避ける
// Before
user.name = "New Name";

// After
const updatedUser = { ...user, name: "New Name" };

// Step 3: 関数を組み合わせる
// Before
const result1 = step1(data);
const result2 = step2(result1);
const result3 = step3(result2);

// After
const process = pipe(step1, step2, step3);
const result = process(data);

関数型プログラミングは、新しい世界の見方だ。データは流れる水のように、関数という水車を通って変換される。状態は凍った氷のように不変。副作用は岸辺に隔離される。

この世界観を身につけた時、あなたのコードは変わる。バグが減り、テストが簡単になり、並行処理が怖くなくなる。

そして何より、プログラミングがもっと楽しくなる。数学的な美しさと実用的な力。それが関数型プログラミングの魅力だ。

始めよう。小さな一歩から。mapから。そして、新しい世界へ。