第15章: ゴシップ好きなバスの運転手¶
はじめに¶
本章では、「ゴシップ好きなバスの運転手」問題を通じて、これまで学んだ関数型デザインパターンを実践的に適用します。この問題は、複数のバス運転手がルートを走りながら情報を交換し、全員が全ての噂を知るまでに何日かかるかを計算するものです。
1. 問題の概要¶
- 複数のバス運転手がいる
- 各運転手は固定のルートを毎日走る
- 運転手は同じ停留所に停まると、お互いの噂を交換する
- 全員が全ての噂を知るまでに何日かかるか?
2. データ構造¶
/// 停留所
pub type Stop = usize;
/// ルート(停留所の循環リスト)
#[derive(Debug, Clone)]
pub struct Route {
stops: Vec<Stop>,
current_index: usize,
}
impl Route {
pub fn new(stops: Vec<Stop>) -> Route {
Route {
stops,
current_index: 0,
}
}
pub fn current_stop(&self) -> Stop {
self.stops[self.current_index]
}
pub fn advance(&self) -> Route {
Route {
stops: self.stops.clone(),
current_index: (self.current_index + 1) % self.stops.len(),
}
}
}
/// 運転手
#[derive(Debug, Clone)]
pub struct Driver {
pub id: usize,
pub route: Route,
pub gossips: HashSet<usize>, // 知っている噂のID
}
impl Driver {
pub fn new(id: usize, route: Route) -> Driver {
let mut gossips = HashSet::new();
gossips.insert(id); // 最初は自分の噂だけ知っている
Driver { id, route, gossips }
}
pub fn current_stop(&self) -> Stop {
self.route.current_stop()
}
pub fn advance(&self) -> Driver {
Driver {
id: self.id,
route: self.route.advance(),
gossips: self.gossips.clone(),
}
}
pub fn share_gossips(&self, other_gossips: &HashSet<usize>) -> Driver {
Driver {
id: self.id,
route: self.route.clone(),
gossips: self.gossips.union(other_gossips).cloned().collect(),
}
}
pub fn knows_all(&self, total_drivers: usize) -> bool {
self.gossips.len() == total_drivers
}
}
3. シミュレーション¶
/// シミュレーション状態
#[derive(Debug, Clone)]
pub struct Simulation {
pub drivers: Vec<Driver>,
pub day: usize,
pub stop: usize, // 1日あたりの停留所数
}
impl Simulation {
pub fn new(routes: Vec<Vec<Stop>>) -> Simulation {
let drivers = routes
.into_iter()
.enumerate()
.map(|(id, stops)| Driver::new(id, Route::new(stops)))
.collect();
Simulation {
drivers,
day: 1,
stop: 0,
}
}
/// 同じ停留所にいる運転手間で噂を交換
fn exchange_gossips(&self) -> Vec<Driver> {
let mut drivers_by_stop: HashMap<Stop, Vec<usize>> = HashMap::new();
// 停留所ごとに運転手をグループ化
for (i, driver) in self.drivers.iter().enumerate() {
drivers_by_stop
.entry(driver.current_stop())
.or_default()
.push(i);
}
// 同じ停留所の運転手間で噂を交換
let mut new_drivers = self.drivers.clone();
for driver_indices in drivers_by_stop.values() {
if driver_indices.len() > 1 {
// 全ての噂を収集
let combined_gossips: HashSet<usize> = driver_indices
.iter()
.flat_map(|&i| self.drivers[i].gossips.iter().cloned())
.collect();
// 各運転手に噂を共有
for &i in driver_indices {
new_drivers[i] = new_drivers[i].share_gossips(&combined_gossips);
}
}
}
new_drivers
}
/// 1停留所進める
pub fn step(&self) -> Simulation {
let drivers_after_exchange = self.exchange_gossips();
let drivers_advanced: Vec<Driver> = drivers_after_exchange
.into_iter()
.map(|d| d.advance())
.collect();
let new_stop = self.stop + 1;
let (day, stop) = if new_stop >= 480 { // 8時間 = 480分(1分1停留所)
(self.day + 1, 0)
} else {
(self.day, new_stop)
};
Simulation {
drivers: drivers_advanced,
day,
stop,
}
}
/// 全員が全ての噂を知っているか
pub fn all_know_everything(&self) -> bool {
let total = self.drivers.len();
self.drivers.iter().all(|d| d.knows_all(total))
}
}
/// シミュレーションを実行
pub fn run_simulation(routes: Vec<Vec<Stop>>, max_days: usize) -> Option<usize> {
let stops_per_day = 480;
let max_stops = max_days * stops_per_day;
let mut sim = Simulation::new(routes);
for _ in 0..max_stops {
if sim.all_know_everything() {
return Some(sim.day);
}
sim = sim.step();
}
None // max_days 以内に完了しない
}
4. DSL によるルート定義¶
/// ルートビルダー
pub struct RouteBuilder {
routes: Vec<Vec<Stop>>,
}
impl RouteBuilder {
pub fn new() -> RouteBuilder {
RouteBuilder { routes: Vec::new() }
}
pub fn driver(mut self, stops: Vec<Stop>) -> RouteBuilder {
self.routes.push(stops);
self
}
pub fn build(self) -> Vec<Vec<Stop>> {
self.routes
}
}
/// DSL マクロ
macro_rules! routes {
($($stops:expr),* $(,)?) => {
RouteBuilder::new()
$(.driver($stops))*
.build()
};
}
// 使用例
let routes = routes![
vec![1, 2, 3],
vec![2, 3, 4],
vec![3, 4, 5],
];
5. テスト例¶
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simple_exchange() {
// 2人の運転手が同じ停留所を共有
let routes = vec![
vec![1, 2, 3],
vec![2, 3, 4],
];
let result = run_simulation(routes, 10);
assert!(result.is_some());
}
#[test]
fn test_no_common_stop() {
// 共通の停留所がない
let routes = vec![
vec![1, 2, 3],
vec![4, 5, 6],
];
let result = run_simulation(routes, 10);
assert!(result.is_none());
}
#[test]
fn test_three_drivers() {
let routes = vec![
vec![3, 1, 2, 3],
vec![3, 2, 3, 1],
vec![4, 2, 3, 4, 5],
];
let result = run_simulation(routes, 10);
assert_eq!(result, Some(2));
}
}
6. パターンの適用¶
この問題では以下のパターンが適用されています:
- 不変データ構造: Driver, Route, Simulation は全て不変
- 関数合成: step() を繰り返し適用
- DSL: routes! マクロによる宣言的な定義
- イテレータ: 効率的なデータ処理
まとめ¶
本章では、「ゴシップ好きなバスの運転手」問題を通じて:
- 不変データ構造による状態管理
- 純粋関数による状態遷移
- DSL による宣言的な問題定義
- イテレータを使った効率的な処理
を学びました。関数型のアプローチにより、テストしやすく、理解しやすいコードが実現できます。
参考コード¶
- ソースコード:
apps/rust/part6/src/chapter15.rs
次章予告¶
次章では、給与計算システムを通じて、より複雑なビジネスロジックの実装を学びます。