第15章: Gossiping Bus Drivers — 6言語統合ガイド¶
1. はじめに¶
Gossiping Bus Drivers は、バス運転手たちが停留所で出会うたびに噂を交換し、全員が同じ噂を共有するまでシミュレーションを続ける問題です。関数型プログラミングの状態遷移、集合演算、無限シーケンスを学ぶ格好の題材です。
2. 共通の本質¶
問題の構造¶
- 各運転手は固定のルートを循環する
- 同じ停留所にいる運転手同士が噂を交換する(集合の和)
- 全運転手が同じ噂の集合を持ったら終了
- 480 ステップ(1 日分)以内に収束しなければ「不可能」
核となるアルゴリズム¶
ループ:
1. 各運転手の現在位置を計算(ルートの循環)
2. 同じ停留所にいる運転手をグループ化
3. 同一グループ内の噂を集合和で統合
4. 全員の噂が一致すれば終了
3. 言語別実装比較¶
3.1 循環ルートの表現¶
| 言語 | 実装方法 | 特徴 |
|---|---|---|
| Clojure | (cycle route) |
遅延無限シーケンス |
| Scala | position % route.length |
剰余演算 |
| Elixir | Stream.cycle(route) |
Stream ベース |
| F# | position % route.Length |
剰余演算 |
| Haskell | cycle route |
遅延無限リスト |
| Rust | current_index % stops.len() |
剰余演算 |
Clojure と Haskell は言語の遅延評価を活かし、cycle で無限シーケンスを直接生成します。他の言語は剰余演算で循環を表現します。
Clojure / Haskell: 遅延無限シーケンス
;; Clojure: 無限列からn番目を取得
(defn current-stop [driver step]
(nth (cycle (:route driver)) step))
-- Haskell: cycle で無限リスト
currentStop :: Driver -> Int -> Stop
currentStop driver step = cycle (driverRoute driver) !! step
Rust / Scala: 剰余演算
pub fn current_stop(&self, step: usize) -> &Stop {
&self.route.stops[step % self.route.stops.len()]
}
def currentStop(step: Int): Stop =
route(step % route.length)
3.2 噂の交換(集合演算)¶
| 言語 | 集合和の方法 |
|---|---|
| Clojure | clojure.set/union |
| Scala | rumors ++ otherRumors |
| Elixir | MapSet.union(a, b) |
| F# | Set.unionMany |
| Haskell | Set.unions |
| Rust | a.union(&b).cloned().collect() |
噂交換の実装比較
;; Clojure
(defn share-rumors [drivers-at-stop]
(let [all-rumors (apply set/union (map :rumors drivers-at-stop))]
(map #(assoc % :rumors all-rumors) drivers-at-stop)))
// Scala
def shareRumors(drivers: Vector[Driver]): Vector[Driver] =
val allRumors = drivers.flatMap(_.rumors).toSet
drivers.map(_.copy(rumors = allRumors))
# Elixir
def share_rumors(drivers) do
all_rumors = Enum.reduce(drivers, MapSet.new(), fn d, acc ->
MapSet.union(acc, d.rumors)
end)
Enum.map(drivers, &%{&1 | rumors: all_rumors})
end
3.3 状態管理のアプローチ¶
| 言語 | 状態の扱い | 更新方法 |
|---|---|---|
| Clojure | 不変マップ | 新しいマップを返す |
| Scala | case class | copy で新インスタンス |
| Elixir | 不変構造体 | %{struct \| field: value} |
| F# | 不変レコード | { record with field = value } |
| Haskell | 不変レコード | レコード更新構文 |
| Rust | 可変 + Clone | clone() で新インスタンス |
全言語が不変の状態遷移を基本とし、各ステップで新しいワールド状態を生成します。
4. 比較分析¶
4.1 遅延評価 vs 剰余演算¶
- 遅延評価(Clojure, Haskell): 概念的に美しく、ルートが無限シーケンスとして自然に表現される
- 剰余演算(Scala, F#, Rust): シンプルで効率的、メモリ使用量が予測可能
4.2 集合演算の表現力¶
Clojure と Haskell は集合演算が最も簡潔です。Rust は所有権システムのため cloned().collect() が必要になりますが、メモリ安全性が保証されます。
5. 実践的な選択指針¶
| 要件 | 推奨言語 | 理由 |
|---|---|---|
| 概念的な明快さ | Clojure, Haskell | 遅延無限シーケンスが問題の本質に合致 |
| パフォーマンス | Rust | ゼロコスト抽象化と所有権管理 |
| 並行シミュレーション | Elixir | BEAM VM の並行処理が自然に拡張可能 |
6. まとめ¶
Gossiping Bus Drivers は、関数型プログラミングの 3 つの要素を統合的に学べる問題です:
- 循環データ: 遅延シーケンス or 剰余演算
- 集合演算: 噂の伝播は集合の和
- 状態遷移: 各ステップが純粋関数