4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Elixir の Mix.State モジュールから学ぶ状態管理の小技

Last updated at Posted at 2025-12-01

はじめに

Elixir の Mix は、プロジェクトの作成・コンパイル・テスト・依存関係管理などを担う標準のビルドツールです。
その内部では、さまざまな「状態」を扱う必要があります。

その一端を担っているのが、Mix.State という内部モジュールです。

Mix.State は公開APIではありませんが、ソースコードを読むと自分のアプリでも使い回せそうな技がいくつも詰まっていました。

ここではざっくりした役割とそこから学べる状態管理の技を、自分用のメモとして整理しておきます。

piyopiyo-board-2025-12.png

※ 写真はイメージです

Mix.State の概要

Mix.State は、次のような責務を持つモジュールです。

  • GenServer として起動される
  • 起動時に ETS の名前付きテーブル を作り、shellenvtarget などの設定を格納
  • 一部の情報は GenServer の状態として保持し、必要になるまで初期化せず、最初の呼び出し時にだけ変換処理を行ってメモ化
  • 読み取り主体の情報は :persistent_term を用いたキャッシュとして保持

大まかには以下のように整理できます:

  • ETS を使った「変更しやすいグローバル状態」
  • persistent_term を使った「読み出し回数が多い値の蓄え」
  • GenServer 内部での「一度だけ計算して後は再利用する値」

この小さなモジュールの中に、状態管理のパターンがコンパクトにまとまっていて、とても参考になります。

Mix.State から学べる技

この記事では、次の 4 つの技に絞って見ていきます。

  1. 名前付き GenServer で ETS テーブルの寿命を管理する
  2. ETS を「グローバル設定用の辞書」として包む
  3. persistent_term で読み出し主体の値を蓄える
  4. GenServer の状態で「遅延初期化+メモ化」を行う

名前付き GenServer で ETS の寿命を管理する

Mix.State はまず GenServer として起動され、その中で ETS テーブルを作成します。

defmodule Mix.State do
  @name __MODULE__
  use GenServer

  def start_link(_opts) do
    GenServer.start_link(__MODULE__, :ok, name: @name)
  end

  @impl true
  def init(:ok) do
    table = :ets.new(@name, [:public, :set, :named_table, read_concurrency: true])

    :ets.insert(table,
      shell: Mix.Shell.IO,
      env: from_env("MIX_ENV", :dev),
      target: from_env("MIX_TARGET", :host),
      scm: [Mix.SCM.Git, Mix.SCM.Path]
    )

    {:ok, %{}}
  end
end

この構成はシンプルですが、いろんな用途で使えるパターンのような氣がします。

  • ETS テーブルは GenServer.init/1 内で生成
  • テーブル名はモジュール名(__MODULE__
  • named_table にすることで外部から参照可能
  • 初期値の挿入を init/1 に集約

このようにしておくことで、次のような利点があります。

  • ETS の寿命が GenServer にひもづく
    プロセスが落ちればテーブルも一緒に消え、再起動すれば自動的に再生成されます。

  • ETS の生成と初期化が一か所に集約される
    状態の立ち上がり方が明確になり、見通しの良い構成になります。

  • supervision tree に統合しやすい
    アプリ全体のライフサイクルに自然に組み込めるため、再起動時の振る舞いも把握しやすくなります。

状態の所有をプロセスに集約しておくことで、「気軽にアクセスできる状態」と「制御された初期化・再構築」の両立が実現できます。

ETS をグローバル設定用の辞書として包む

Mix.State では、ETS を直接操作せず、専用のラッパ関数を通じて状態にアクセスできるようにしています。

def get(key, default \\ nil) do
  case :ets.lookup(@name, key) do
    [{^key, value}] -> value
    [] -> default
  end
end

def put(key, value) do
  :ets.insert(@name, {key, value})
  :ok
end

def fetch(key) do
  case :ets.lookup(@name, key) do
    [{^key, value}] -> {:ok, value}
    [] -> :error
  end
end

def update(key, fun) do
  :ets.insert(@name, {key, fun.(:ets.lookup_element(@name, key, 2))})
  :ok
end

こうすることで、呼び出し側では Mix.State.get(:env) のように、ETS の存在を意識せず扱えます。

persistent_term で読み出し主体の値を蓄える

Mix.State は、頻繁に読み出されるがほとんど更新されない値を :persistent_term で保持しています。

def read_cache(key) do
  :persistent_term.get({__MODULE__, key}, nil)
end

def write_cache(key, value) do
  :persistent_term.put({__MODULE__, key}, value)
  value
end

def delete_cache(key) do
  :persistent_term.erase({__MODULE__, key})
end

def clear_cache do
  for {{__MODULE__, _} = key, _value} <- :persistent_term.get() do
    :persistent_term.erase(key)
  end
end

このように設計すれば、persistent_term を高速な読み取り専用キャッシュとして使えます。

初期化時に一度だけ設定し、その後ほとんど更新されない値に向いています。

GenServer 状態での遅延初期化とメモ化

Mix.State では、必要になるまで処理を遅らせ、最初の呼び出し時に一度だけ実行・保存するパターンが使われています。

def handle_call(:builtin_apps, _from, %{builtin_apps: apps} = state) do
  if is_map(apps) do
    {:reply, apps, state}
  else
    result =
      for path <- apps,
          app = app_from_code_path(path),
          do: {app, path},
          into: %{}

    {:reply, result, %{state | builtin_apps: result}}
  end
end

初期状態では「変換前の値」を持っておき、
最初の問い合わせ時だけ変換処理を行い、その結果を state にメモ化しています。

このような遅延初期化は、「重い処理だけど毎回は不要」なケースに効果的です。初回の呼び出しでのみ処理を実行し、それ以降はキャッシュされた値を返す設計になっており、並行アクセス時も GenServer が直列化を保証してくれるため、安全に一度だけ実行されます。状態変換のタイミングが明確になることで、コード全体の見通しも良くなります。

おわりに

Mix.State はごく小さな内部モジュールですが、その中には汎用性の高い状態管理の技がいくつも詰まっていました。

普段何気なく使っている標準ライブラリでも、ソースコードを読むことで自分のコードにも取り入れられるアイデアが見つかることがあり、あらためて学びの多さを実感しています。

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?