Upgrade to Pro — share decks privately, control downloads, hide ads and more …

関数型プログラミングのメリットって何だろう?

 関数型プログラミングのメリットって何だろう?

Avatar for わんこ(Wanko_IT)

わんこ(Wanko_IT)

July 07, 2026

More Decks by わんこ(Wanko_IT)

Other Decks in Programming

Transcript

  1. 01 ─ 関数型プログラミングとは何か 関数型プログラミングの定義 そもそも、関数型プログラミングとは何でしょうか? 調べると、以下のような特徴が挙げられています。 参照透過性 同じ入力には常に同じ出力を 返す 値の不変性

    一度作られた値は決して変更 されない 第一級関数 関数を値として扱い、引数や 戻り値にできる 高階関数の利用 関数を受け取る・返す関数を 活用するスタイル …ただし、広く認められた共通の定義は確立されていない。そこで今回は、ある定義を採用します。
  2. 02 ─ 値の不変性とは何か 値の不変性 Java・C・PHP・Ruby などのオブジェクト指向・手続き型の言語は変数(変更可能な値)で状態を表現する。一方で Clojure・Scala・Erlang などの関数型志向の言語は、定数(不変な値)を基本とする。 非関数型(Java)— 変数を書き換える

    class Wallet { private int money; public Wallet(int money) { this.money = money; } public int getMoney() { return money; } public void addMoney(int amount) { money = money + amount; // 中身を書き換える } } Wallet wallet = new Wallet(1000); wallet.getMoney(); // 1000 wallet.addMoney(500); wallet.getMoney(); // 1500 ← 状態が変わった 関数型(Scala)— 新しい値を作る case class Wallet(money: Int) def addMoney(wallet: Wallet, amount: Int): Wallet = Wallet(wallet.money + amount) // 新しい財布を作る val wallet1 = Wallet(1000) val wallet2 = addMoney(wallet1, 500) wallet1.money // 1000 ← 変わっていない! wallet2.money // 1500 ← 新しくできた財布 関数型では、値は定数が基本。状態の変化は「既存の値をもとに作られた新しい定数」として表現する。
  3. 03 ─ 値が不変だと何が嬉しいのか 問題① 共有ミュータブル状態(Java) Q. このコード、何が出力されるでしょう? 月末ごとの残高を記録するクラス class BalanceHistory

    { // 特定地点での財布の状態を保持 private List<Wallet> snapshots = new ArrayList<>(); // 「今この瞬間の状態」を記録 public void recordSnapshot(Wallet wallet) { snapshots.add(wallet); } // 記録された履歴を取得 public List<Wallet> getSnapshots() { return snapshots; } } 利用側のコード Wallet wallet = new Wallet(1000); BalanceHistory history = new BalanceHistory(); history.recordSnapshot(wallet); // 1月末: 1000円を記録 wallet.addMoney(500); history.recordSnapshot(wallet); // 2月末: 1500円を記録 wallet.addMoney(-1500); history.recordSnapshot(wallet); // 3月末: 0円を記録 for (Wallet snap : history.getSnapshots()) { System.out.println(snap.getMoney()); }
  4. 03 ─ 値が不変だと何が嬉しいのか 原因: 3要素は同一インスタンス recordSnapshot(Wallet wallet) の引数は参照渡し。リストに追加されるのはコピーではなく、同じWalletへの 参照。3要素が指す実体は1つで、その値は最後の変更(3月末=0円)の状態になっている。 ①

    リストの中身を図にすると snapshots[0] ──▶ snapshots[1] ──▶ snapshots[2] ──▶ Wallet(money = 0) たった1つの同じインスタンス ② コードで要素の等価性を確認しても、同一 snapshots.get(0) == snapshots.get(1) // true snapshots.get(0) == snapshots.get(2) // true snapshots.get(1) == snapshots.get(2) // true 共有ミュータブル状態では、処理の影響が予期せぬ形で波及する。アプリ全体を正常に保つために「考えるべき こと」が増大してしまう。
  5. 03 ─ 値が不変だと何が嬉しいのか 不変な値による解決(Scala) case class BalanceHistory(snapshots: List[Wallet] = Nil)

    { def recordSnapshot(wallet: Wallet): BalanceHistory = BalanceHistory(snapshots :+ wallet) } // 1月末: 1000円 val wallet1 = Wallet(1000) val history1 = BalanceHistory().recordSnapshot(wallet1) // 2月末: 1500円 val wallet2 = wallet1.addMoney(500) val history2 = history1.recordSnapshot(wallet2) // 3月末: 0円 val wallet3 = wallet2.addMoney(-1500) val history3 = history2.recordSnapshot(wallet3) history3.snapshots.foreach(w => println(w.money)) // 1000, 1500, 0 ← 期待どおり! ポイント 1 プログラム中のすべての値は変更不能かつ 異なる実体であると、言語が保証する ポイント 2 異なる状態のWalletは別々の実体として保 存され、後から変更される心配がない ポイント 3 「スナップショット」が言葉どおりの意味 で機能し、記録した瞬間の状態がそのまま 残る 出力: 1000 → 1500 → 0 ✓
  6. 03 ─ 値が不変だと何が嬉しいのか 問題② 時間的結合(Java) class Wallet { private int

    balance = 1000000; // 残高(100万円) private int count = 0; // 送金回数 // 送金を行うメソッド public void transfer(int amount) { balance -= amount; // ← この瞬間「残高だけ減って回数は0のまま」 // という業務上ありえない財布が存在する count += 1; } } balance と count はセットで変わるべき値だが、 変数は1つずつしか書き換えられないため、途中に 不正な中間状態が実在する瞬間が生まれる。 その結果「この瞬間に参照・操作してはいけない」 という時間軸上の制約が生まれる。コードのどこ にも書かれていないこの依存が時間的結合。 この一瞬を別スレッドが読み取れば再現困難なバグに。 あいだで例外が起きれば不整合なまま残り続ける。 transfer() 実行中の時間軸 balance -= amount; ▶ 不正な中間状態 残高: 99万円 / 送金回数: 0回 ▶ count += 1;
  7. 03 ─ 値が不変だと何が嬉しいのか 同時に確定する状態(Scala) case class Wallet(balance: Int, count: Int)

    { def transfer(amount: Int): Wallet = Wallet(balance - amount, count + 1) // ↑ 残高と回数が同時に確定した新しい財布 } val w0 = Wallet(1000000, 0) val w1 = w0.transfer(10000) // w0: (1000000, 0) … 送金前の財布 // w1: ( 990000, 1) … 送金後の財布 // 「残高だけ減った財布」は書きようがない 不変な値では、状態の変化を全フィールドが同時 に確定した新しい値としてしか表現できない。 つまり不正な中間状態が、そもそも実在しない。 存在するのは送金前と送金後、2つの正しい財布 だけ。 中間状態が消えれば「この瞬間に参照・操作して はいけない」という制約も丸ごと消える — 守るべ きルールではなく、ルールが必要な状況自体をな くす。 可変(Java) 1つの財布が時間とともに姿を変え、途中の不正な姿 が存在してしまう。 不変(Scala) 存在するのは w0 と w1 という2つの完成品だけ。途 中がない。
  8. 04 ─ まとめ: なぜこれが重要か “Of the three manifestations of complexity,

    unknown unknowns are the worst.” John Ousterhout『A Philosophy of Software Design, 2nd Edition』 ソフトウェアの複雑性のうち最悪なのが「未知の未知」— 問題が潜んでいるかどうかさえ、知る術がないこ と。バグが出たとき、初めてその存在に気づく。 ここまで見てきた可変な値に起因するバグは、特定の手順・条件でしか起きない。そのため動作検証やテス トをすり抜け、バグが顕在化するまで問題の存在に気づけない — 未知の未知そのものである。 不変な値なら、不整合な状態はそもそも作れない。「未知の未知」になる前に、言語が芽を摘んでくれる。
  9. 04 ─ まとめ まとめ 関数型プログラミングとは、不正な状態を発生させにくくするプログラミングスタイルで ある。 1 本質は不変性 — あらゆる値を定数として扱う

    状態の変化は「既存の値をもとにした新しい定数」として表現する 2 共有ミュータブル状態と時間的結合を構造的に排除できる 予期せぬ波及も、不正な中間状態も、そもそも表現できない 3 銀の弾丸ではないが、合理的な選択肢 再現困難でテストをすり抜ける「未知の未知」型バグの発生確率を下げられる ご清聴ありがとうございました