HomeOS を関数型で作る — 計画 (仮)

[仮版・project plan] 関数型祭りに向けて、自宅の HomeOS(家の自動化基盤)を関数型プログラミングで「それなりに範囲広く」作る計画。言語は Elixir (BEAM)、配置は local-first で確定。実機はまだ無いのでシミュレータ先行。このノートはプロジェクトの起点ドキュメントで、決定事項・設計原則・フェーズ計画を固定する。

なぜ Elixir (BEAM) なのか

HomeOS という問題の形が、Erlang が生まれた問題(電話交換機)とほぼ同型だから。「無難だから」ではなく構造が一致しているから選ぶ。

家の自動化を分解すると:

これは「軽量プロセス1個 = デバイス/部屋/ルール1個」「let it crash + Supervisor で再起動」「メッセージパッシングで疎結合」という BEAM のモデルにそのまま落ちる。1個のデバイスがクラッシュしても家全体は落ちない、を設計の努力ではなくランタイムが保証する。 これが他言語に対する決定的な差。

対抗馬の整理

言語立ち位置不採用理由(今回)
Elixir ✅BEAM 上の実用関数型。OTP / Phoenix / Nerves— 採用
GleamBEAM 上の静的型付き。型 + OTP祭り映えは最強だが OTP 生態系が若い。一部ロジックで同居は検討(補助)
Haskell純粋・型駆動・FRPI/O 泥臭い統合と常時稼働運用が不利。OTP 相当は自作
F#.NET 全資産 + 実用関数型主張が弱め。今回は不要
ClojureJVM・データ指向・REPL駆動動的型。デモ映えはするが堅牢さ優先で外す

関連: functional-programming / pure-functional-programming / algebraic-effects / adt-gadt(ルールを ADT で表現する設計に効く)

確定事項

項目決定
言語Elixir (BEAM) + Phoenix LiveView(補助に Gleam は任意)
配置完全 local-first。Hub = 既存の常時稼働箱(NAS/NUC/旧PC 等、SSD 付き)
実機まだ無い → シミュレータ先行でソフトを完成させ、実機は後から同インターフェースに差す
クラウド大きなクラウドは使わない。外向きは LLM API のアウトバウンドのみ(任意・落ちてOK)。遠隔操作は Tailscale
LLMClaude API(Haiku=高速安価 / Sonnet=ルール生成)、任意でローカル小モデル(Bumblebee)フォールバック

外部依存が「Tailscale メッシュ」と「任意の LLM 呼び出し」だけになり、家の中核は1台で自己完結する。

設計原則

1. local-first(頭脳をクラウドに置かない)

家を動かすシステムをクラウド依存にするのは堅牢さの観点では設計バグ:

→ 頭脳(Hub・自動化エンジン・実機制御)は家の中で完結させ、クラウドは「薄くて差し替え可能なエッジ」に限定する。判断軸は edge-vs-cloud-vs-onprem を参照。

2. クラウドの役割は限定する

仕事採用
遠隔アクセスTailscale(ポート開放ゼロ・クラウド頭脳不要)
LLM 連携Claude API へのアウトバウンド呼び出しのみ(クリティカルパス外)
バックアップ/OTA当面ローカル。必要なら Hetzner/Fly の小箱を後付け

AWS/GCP のフルマネージドは家用途には過剰・高コスト・依存増のため不採用。

3. LLM はクリティカルパスから外す

LLM の仕事は「翻訳」だけ: 自然言語(「寝るモードにして」)→ 型付きの %Command{} か %Rule{} に変換。実行は決定論エンジンが担う。

アーキテクチャ(3層)

┌─ ③ 知能エッジ(任意・落ちてOK)─────────────┐
│   Brain GenServer → Claude API / ローカル小モデル  │
│   自然言語 →「検証済み %Command / %Rule」に変換だけ │
└───────────────┬─────────────────────────┘
                ↓ 検証済みデータのみ流す(生出力は実機に触れさせない)
┌─ ② インターフェース ──────────────────────┐
│   LiveView ダッシュボード / Tailscale 経由の遠隔   │
└───────────────┬─────────────────────────┘
                ↓
┌─ ① 制御コア(決定論・常時稼働)───────────────┐
│   Supervisor + Device GenServers + Rule Engine + Bus │
│   ★ここだけで家は完全に動く。①は②③が死んでも無傷   │
└─────────────────────────────────────────┘

スーパービジョンツリー(堅牢さの本体)

# application.ex
children = [
  HomeOS.Repo,                              # 履歴(SQLite/Postgres via Ecto)
  {Phoenix.PubSub, name: HomeOS.PubSub},    # 内部イベントバス
  {Registry, keys: :unique, name: HomeOS.DeviceRegistry},
  HomeOS.MQTT.Connection,                   # Tortoise (実機導入後に有効化)
  HomeOS.Ingest.Pipeline,                   # Broadway: センサ洪水を背圧制御
  {DynamicSupervisor, name: HomeOS.Devices.Sup},     # 1デバイス=1プロセス
  {DynamicSupervisor, name: HomeOS.Automations.Sup}, # 1ルール=1プロセス
  HomeOS.Brain,                             # LLM 境界(任意・circuit breaker 付き)
  HomeOS.Scheduler,                         # cron(Quantum)
  HomeOSWeb.Endpoint                        # LiveView
]
Supervisor.init(children, strategy: :one_for_one)

自動化を「データ + 純粋関数」で持つ(関数型の本領)

%HomeOS.Rule{
  id: "夜の人感ライト",
  trigger: {:device, "hall_motion"},
  condition: fn ctx -> ctx.sun == :night and ctx.hall_motion == :detected end,
  action: {:set, "hall_light", :on, ttl: {:minutes, 5}}
}

ルールは値。だからテストできる(純粋関数)/シリアライズして DB 保存できる/LiveView や LLM から無停止で追加・差し替えできる。エンジンはイベントを畳み込む reducer に過ぎない。

フェーズ計画

フェーズ内容完了の定義
P0 土台リポジトリ / CI(GH Actions) / Tailscale / Hub セットアップmix phx.new が Hub 上で起動
P1 堅牢コア + シミュレータSupervisor+Registry+PubSub、Device GenServer、仮想デバイス、LiveView 表示プロセスを kill→自己回復、画面がリアルタイム更新
P2 自動化エンジンRule as data、純粋エンジン、無停止差し替え、テスト「人感→点灯」ルールが値として追加・テストできる
P3 LLM 連携Brain 境界 GenServer、structured output→検証→提案→適用、circuit breaker自然言語で %Rule{} 生成→承認→ホットロード
P4 遠隔 & 運用Tailscale 遠隔、:telemetry/LiveDashboard、バックアップ外からダッシュボード操作、メトリクス可視化
P5 実機統合 → 拡大Zigbee2MQTT 導入で仮想→実機、在室/電力/通知へ拡張実機1個がシミュレータと同じコードで動く

実機が無いのはむしろ好都合。 P1〜P4 を全部シミュレータ(仮想デバイスが PubSub に状態を流すだけ)で完成でき、ノートPC1台で「殺して直る」「LLM 切っても動く」を実演できる。実機は P5 で同じインターフェースに差し込むだけ。

「範囲を広く」は同じ4部品(Registry・PubSub・Supervisor・Rule)の反復で伸びる。在室検知 / 電力モニタ / 通知 / 音声 / スケジュール / 履歴分析 ── どれも「バスに繋がる新しいプロセス群 + 新しいルール」でしかなく、認知コストが線形にしか増えない。

祭りデモシナリオ

  1. プロセスを殺して自己回復 ── デバイスプロセスを Process.exit で落とし、Supervisor が即座に復帰させる様子を見せる
  2. 家を止めず自然言語でルール追加 ── LiveView から「夜に廊下で動いたら5分点灯」と入力 → LLM が %Rule{} 生成 → 承認 → 無停止でホットロード
  3. LLM を切断しても家は動く ── ネットを切り、既存ルールで自動化が継続することを実演(local-first の証明)

Open questions

関連

Graph