Skip to content

第19章: Wa-Tor シミュレーション — 6言語統合ガイド

1. はじめに

Wa-Tor は、トーラス型の 2D グリッド上で魚とサメの生態系をシミュレートするセルオートマトンです。関数型プログラミングの不変状態管理再帰的データ処理多態的ディスパッチを大規模に実践する総合的なケーススタディです。

2. 共通の本質

シミュレーションルール

生物 ルール
隣接する空きセルに移動。一定年齢で繁殖(子を残して移動)
サメ 隣接する魚を捕食(エネルギー回復)。魚がいなければ空きセルに移動。エネルギー切れで死亡。一定年齢で繁殖
空きセル。魚やサメの移動先

トーラス座標

グリッドの端がつながったトーラス構造:

x' = ((x % width) + width) % width
y' = ((y % height) + height) % height

各ステップの処理

1. すべてのセルを走査
2. 各生物のルールを適用(移動・捕食・繁殖・死亡)
3. 新しいワールド状態を生成

3. 言語別実装比較

3.1 グリッドの表現

言語 グリッド型 セル表現 隣接方向
Clojure Map<[x,y], Cell> キーワードマップ 8 方向(Moore)
Scala Map[Position, Cell] sealed trait 8 方向
Elixir Map<{x,y}, Cell> マップ / nil(水) 4 方向(Von Neumann)
F# Map<Position, Cell> 判別共用体 4 方向
Haskell Map Location Cell ADT 8 方向
Rust Vec<Creature> struct + enum 4 方向(スパース)
Clojure: マップベースのグリッド
(defn make-world [width height]
  {:type ::world :width width :height height
   :cells {}})

(defn make-fish []
  {:type ::fish :age 0})

(defn make-shark [health]
  {:type ::shark :age 0 :health health})

;; トーラス座標のラップ
(defn wrap [{:keys [width height]} [x y]]
  [(mod (+ x width) width)
   (mod (+ y height) height)])
Scala: sealed trait + case class
sealed trait Cell
case object Water extends Cell
case class Fish(age: Int = 0) extends Cell
case class Shark(age: Int = 0, health: Int) extends Cell

case class World(
  width: Int, height: Int,
  cells: Map[Position, Cell],
  generation: Int = 0
)
Haskell: ADT + Map
data Cell
    = WaterCell
    | FishCell { fishAge :: Int, fishReproductionAge :: Int }
    | SharkCell { sharkAge :: Int, sharkReproductionAge :: Int, sharkEnergy :: Int }

data World = World
    { worldWidth  :: Int
    , worldHeight :: Int
    , worldCells  :: Map Location Cell
    , worldFishReproAge :: Int
    , worldSharkReproAge :: Int
    , worldSharkInitialEnergy :: Int
    }
Rust: スパースな生物リスト
pub enum CreatureType { Fish, Shark }

pub struct Creature {
    pub creature_type: CreatureType,
    pub position: Position,
    pub energy: i32,
    pub breed_time: i32,
}

pub struct World {
    pub width: i32,
    pub height: i32,
    pub creatures: Vec<Creature>,
    pub fish_breed_time: i32,
    pub shark_breed_time: i32,
}
Rust はグリッド(`Map`)ではなく**生物リスト(`Vec`)**を使用します。密度が低いワールドでのメモリ効率が高くなります。

3.2 状態更新のアプローチ

言語 更新方法 乱数の扱い
Clojure assoc / update で新マップ生成 rand-int(暗黙的)
Scala copy + Map.updated Random(暗黙的)
Elixir Map.put で新マップ生成 :rand.uniform(暗黙的)
F# Map.add で新マップ生成 System.Random(明示的参照)
Haskell Map.insert で新マップ生成 StdGen明示的に引き回し
Rust Vec の clone + 変更 rand::Rng(明示的)
Haskell: 乱数の明示的な引き回し
tickWorld :: World -> StdGen -> (World, StdGen)
tickWorld world gen =
    foldl' tickLocation (world, gen) allLocations
  where
    allLocations = [(x, y) | x <- [0..worldWidth world - 1]
                           , y <- [0..worldHeight world - 1]]

tickLocation :: (World, StdGen) -> Location -> (World, StdGen)
tickLocation (world, gen) loc =
    case Map.lookup loc (worldCells world) of
        Just (FishCell age _) -> tickFish world loc age gen
        Just (SharkCell age _ energy) -> tickShark world loc age energy gen
        _ -> (world, gen)
Haskell は純粋関数型言語のため、乱数生成器 `StdGen` を**引数と戻り値で明示的に受け渡し**します。これにより参照透過性が保たれ、同じシードで同じ結果を再現できます。

3.3 魚のティック処理

魚の移動・繁殖ロジック比較
;; Clojure
(defmethod tick ::fish [world loc]
  (let [cell (get-cell world loc)
        neighbors (empty-neighbors world loc)
        can-reproduce? (>= (:age cell) fish-reproduction-age)]
    (if (seq neighbors)
      (let [target (rand-nth neighbors)]
        (if can-reproduce?
          (-> world
              (set-cell target (assoc cell :age 0))
              (set-cell loc (make-fish)))
          (-> world
              (set-cell target (update cell :age inc))
              (set-cell loc (make-water)))))
      (update-in world [:cells loc :age] inc))))
// Scala
def tickFish(world: World, pos: Position, fish: Fish): World =
  val emptyNeighbors = getEmptyNeighbors(world, pos)
  if emptyNeighbors.nonEmpty then
    val target = emptyNeighbors(Random.nextInt(emptyNeighbors.size))
    val aged = fish.copy(age = fish.age + 1)
    if aged.age >= world.fishBreedTime then
      world.updated(target, Fish()).updated(pos, Fish())
    else
      world.updated(target, aged).updated(pos, Water)
  else
    world.updated(pos, fish.copy(age = fish.age + 1))
-- Haskell
tickFish :: World -> Location -> Int -> StdGen -> (World, StdGen)
tickFish world loc age gen =
    let emptyNeighbors = getEmptyNeighbors world loc
    in if null emptyNeighbors
       then (updateCell world loc (FishCell (age + 1) reproAge), gen)
       else let (idx, gen') = randomR (0, length emptyNeighbors - 1) gen
                target = emptyNeighbors !! idx
            in if age >= reproAge
               then (reproducefish world loc target, gen')
               else (moveFish world loc target age, gen')

3.4 可視化

言語 可視化方法
Clojure Quil(Processing ラッパー)で GUI
Scala ASCII + DSL
Elixir ASCII ターミナル出力
F# ASCII + 統計表示
Haskell ASCII + 統計表示
Rust ASCII ターミナル出力

4. 比較分析

4.1 設計アプローチの違い

アプローチ 言語 利点
グリッドベース(密) Clojure, Scala, F#, Haskell 隣接セルの参照が O(1)
生物リスト(疎) Rust メモリ効率が高い(低密度時)
nil = 水 Elixir マップのサイズが小さい
明示的 WaterCell Clojure, Haskell 水セルも明示的に管理

4.2 純粋性と乱数

言語 乱数の純粋性 テストの再現性
Haskell 完全に純粋(StdGen を引き回し) シードで完全再現
Rust 明示的だが副作用あり シードで再現可能
Clojure, Scala, Elixir, F# 暗黙的な副作用 シード設定で再現可能

Haskell のアプローチは最も厳密ですが、コードの複雑さが増します。他の言語は実用的なトレードオフとして暗黙的な乱数を使用しています。

4.3 パフォーマンス特性

言語 メモリ管理 大規模ワールドでの効率
Clojure 永続データ構造(構造共有) 中程度
Scala 不変 Map + copy 中程度
Elixir 不変 Map 中程度
F# 不変 Map 中程度
Haskell 不変 Map + 遅延評価 遅延により効率化の可能性
Rust Vec + clone 高い(スパース表現)

5. 実践的な選択指針

要件 推奨言語 理由
GUI 可視化 Clojure Quil による豊富なグラフィックス
テストの完全再現性 Haskell 純粋な乱数管理
大規模シミュレーション Rust スパース表現 + ゼロコスト抽象化
分散シミュレーション Elixir プロセス分散が容易
DSL による構築 Scala ビルダーパターンの表現力

6. まとめ

Wa-Tor シミュレーションは、関数型プログラミングの総合力を試すケーススタディです:

  1. 不変状態管理: 各ステップで新しいワールド状態を生成
  2. 多態的ディスパッチ: 魚・サメ・水を統一的に処理
  3. 乱数と純粋性: Haskell の明示的アプローチ vs 他言語の実用的トレードオフ
  4. データ表現の選択: グリッド vs スパースリスト

言語別個別記事