はじめに
(Elixir言語は)初投稿です。
古来より伝わる「ライフゲーム」をモダンな言語で実装するチャレンジやっております。
今回は最近知ったElixir言語でやってみます。
過去シリーズは以下です。
開発環境
- MacStudio2023(M2 Max)
- macOS Tahoe(26.1)
- Erlang/OTP 27
- Elixir 1.19.3
ライフゲームとは
概要は先駆者様の解説に委ねます。
コーディングに必要なルールは以下となります。
- 2次元グリッド上で展開され、各セルは「生」または「死」の2つの状態を持つ。
- 各世代で、「隣接(上下左右と斜め4方向の計8方向にある)」セルを以下のルールに従って状態が更新される。
- 誕生:死んでいるセルに隣接する生きたセルがちょうど3つあれば、次の世代で誕生する。
- 生存:生きているセルに隣接する生きたセルが2つまたは3つならば、次の世代でも生存する。
- 過疎:生きているセルに隣接する生きたセルが1つ以下ならば、過疎により死滅する。
- 過密::生きているセルに隣接する生きたセルが4つ以上ならば、過密により死滅する。
- 端は常に死んでいるとみなす(固定境界)
各コードについての知見
コードそのものよりも、ほぼオブジェクト指向言語しか習得していなかった私が感じたElixirの特徴を語ります。
クラス…ではなくモジュール
まず最初の洗礼が「クラスの概念を捨て」なければならない所でした。
defmodule Lifegame do
機能のまとめ役が、当然のように継承のような拡張指定が存在しないシンプルイズ最強と言わんばかり(個人の見解です)の潔さ。
また「defmodule」という名称と、Mixでプロジェクト作成した時にコード置き場は「lib」フォルダになるところから、Elixir言語はモジュール制作を一貫するという強い意志を感じました。
永続的メモリ確保もしない
衝撃もさめやまぬうちにすぐに「メンバのような永続的に生存する変数などは無い」という洗礼を受けました。
ここからElixir言語がメモリ領域を汚すことを極力避けることに初志貫徹する強い意志を感じました。
関数記述のエキセントリックさ
関数の記述に関してもかなり独特な印象です。
def new(w, h) do
for pos_y <- 0..(h - 1) do
for pos_x <- 0..(w - 1) do
dead_or_alive(pos_y * w + pos_x)
end
end
end
特に衝撃だったのが以下です。
- 関数は必ず戻り値が存在する、よって「return」のような記述は無い
- 戻り時は関数内で最後に記述した関数で決定する
- for文内の関数が最後の記述だった場合は、その関数の戻り値の集合を返す
- 戻り時は関数内で最後に記述した関数で決定する
これも「関数の仕様を限定」することで複雑化を極力避けるという強い意志を感じました。
記述順番を前提とした多態表現
関数の記述で順番を重視するというのもその発想はなかった的衝撃でした。
# 生存:生きているセルに隣接する生きたセルが2つかまたは3つあれば、生存維持
# 過疎or過密:生きているセルが上記の条件を満たさない場合は、次の世代で死滅する
# 誕生:死んでいるセルに隣接する生きたセルがちょうど3つあれば、次の世代で誕生する
# 過疎or過密:死んでいるセルが上記の条件を満たさない場合は、次の世代で死滅する
defp rule(1, alive_neighbors) when alive_neighbors in 2..3, do: 1
defp rule(1, _), do: 0
defp rule(0, 3), do: 1
defp rule(0, _), do: 0
関数の多態表現を別個の関数として定義をするのは独特ですが、同時に見やすさも感じました。
一般的な言語なら関数内でif文かswitch文を駆使して記述する方法は、可読性の低下やインデントの洪水が起こりがちだという問題があることを痛感しました。
ここからElixir言語が、「インデントは人を狂わせる」という未解決問題に一石を投じようという強う意志を勝手に感じています。
その他気付きを感じた仕様
「@」を用いたドキュメント記述
# モジュールの説明
defmodule Lifegame do
@moduledoc """
ライフゲームのコントローラーモジュール
"""
# カスタム型表記の設定
@type grid_type :: [[0 | 1]]
# 関数の引数と戻り値の説明
@spec rule(number(), number()) :: number()
defp rule(1, alive_neighbors) when alive_neighbors in 2..3, do: 1
# パブリック関数限定で公開仕様の記述
@doc """
指定された幅と高さで新しいグリッドを作成、生=1死=0の決定、2次元配列を返す
"""
@spec new(integer(), integer()) :: grid_type
def new(w, h) do
for pos_y <- 0..(h - 1) do
for pos_x <- 0..(w - 1) do
dead_or_alive(pos_y * w + pos_x)
end
end
end
パイプライン処理の表記
@doc """
グリッドの状態を文字列に変換して返す
"""
@spec format_grid(grid_type) :: String.t()
def format_grid(grid) do
# パイプライン表記
grid
|> Enum.map(fn row ->
Enum.map(row, fn
1 -> "■"
0 -> "□"
end)
|> Enum.join(" ")
end)
|> Enum.join("\n")
end
ループ処理は基本は関数の再帰呼び出しで記述
defp loop(grid, generation, max_generation) do
# 再帰呼び出し
loop(next(grid), generation + 1, max_generation)
end
コード全体
極めてライフゲームの基礎だけなので、説明は省略します。
defmodule Lifegame do
@moduledoc """
ライフゲームのコントローラーモジュール
"""
@type grid_type :: [[0 | 1]]
# 疑似乱数で生死の決定
@spec dead_or_alive(number) :: 0 | 1
defp dead_or_alive(index) do
cond do
rem(index, 5) == 0 -> 1
rem(index, 11) == 0 -> 1
true -> 0
end
end
@doc """
指定された幅と高さで新しいグリッドを作成、生=1死=0の決定、2次元配列を返す
"""
@spec new(integer(), integer()) :: grid_type
def new(w, h) do
for pos_y <- 0..(h - 1) do
for pos_x <- 0..(w - 1) do
dead_or_alive(pos_y * w + pos_x)
end
end
end
@doc """
現在のグリッドの状態から次の世代を計算、2次元配列を返す
"""
@spec next(grid_type) :: grid_type
def next(grid) do
# 第1次元配列の個数
h = length(grid)
# 第2次元配列の個数
w = length(hd(grid))
# 2重ループでセルを処理
for y <- 0..(h - 1) do
for x <- 0..(w - 1) do
# 現在のセルの生死
cell = Enum.at(Enum.at(grid, y), x)
# 隣接するセルの生死を数える
alive_neighbors = count_alive_neighbors(grid, x, y, w, h)
# 生存または誕生するかを判定
rule(cell, alive_neighbors)
end
end
end
# 生きているセルの隣接するセルの数を数える
@spec count_alive_neighbors(grid_type, number(), number(), integer(), integer()) :: integer()
defp count_alive_neighbors(grid, x, y, w, h) do
# 隣接するセル座標でループ
for dy <- -1..1, dx <- -1..1, {dx, dy} != {0, 0} do
nx = x + dx
ny = y + dy
# 隣接範囲内チェック
if nx >= 0 and nx < w and ny >= 0 and ny < h do
# 隣接範囲内のセルの生死
Enum.at(Enum.at(grid, ny), nx)
else
# 端は常に死んでいるとみなす(固定境界)
0
end
end
|> Enum.sum()
end
# 生存:生きているセルに隣接する生きたセルが2つかまたは3つあれば、生存維持
# 過疎or過密:上記の条件を満たさない場合は、次の世代で死滅する
# 誕生:死んでいるセルに隣接する生きたセルがちょうど3つあれば、次の世代で誕生する
# 過疎or過密:上記の条件を満たさない場合は、次の世代で死滅する
@spec rule(number(), number()) :: number()
defp rule(1, alive_neighbors) when alive_neighbors in 2..3, do: 1
defp rule(1, _), do: 0
defp rule(0, 3), do: 1
defp rule(0, _), do: 0
@doc """
グリッドの状態を文字列に変換して返す
"""
@spec format_grid(grid_type) :: String.t()
def format_grid(grid) do
grid
|> Enum.map(fn row ->
Enum.map(row, fn
1 -> "■"
0 -> "□"
end)
|> Enum.join(" ")
end)
|> Enum.join("\n")
end
@doc """
ライフゲーム起動
"""
@spec run(integer(), integer()) :: :ok
def run(w \\ 17, h \\ 11) do
grid = new(w, h)
max_generation = 10
loop(grid, 0, max_generation)
end
@spec loop(grid_type, integer(), integer()) :: :ok
defp loop(grid, generation, max_generation) do
# 世代表示
IO.puts("世代: #{generation} #{if generation == 0, do: "(初期状態)", else: ""}")
# グリッド表示
IO.puts(format_grid(grid))
# ミリ秒待機
Process.sleep(500)
# 有限実施
if generation < max_generation do
# 再帰呼び出し
loop(next(grid), generation + 1, max_generation)
else
:ok
end
end
end
実行結果例
世代: 0 (初期状態)
■ □ □ □ □ ■ □ □ □ □ ■ ■ □ □ □ ■ □
□ □ □ ■ □ ■ □ □ ■ □ □ □ □ ■ □ □ ■
□ ■ □ □ □ □ ■ □ □ □ ■ ■ □ □ □ □ ■
□ □ □ □ ■ □ □ □ □ ■ □ □ □ □ ■ ■ □
□ □ ■ □ □ □ □ ■ □ ■ □ □ ■ □ □ □ □
■ □ □ ■ □ ■ □ □ □ □ ■ □ □ □ ■ ■ □
□ □ □ ■ □ □ □ □ ■ □ □ □ □ ■ □ □ □
□ ■ ■ □ □ □ ■ □ □ □ □ ■ □ ■ □ □ ■
□ □ □ □ ■ □ □ ■ □ ■ □ □ □ □ ■ □ □
□ ■ ■ □ □ □ □ ■ □ □ □ □ ■ □ □ □ □
■ □ □ □ □ ■ ■ □ □ □ ■ □ □ □ □ ■ □
世代: 1
□ □ □ □ ■ □ □ □ □ □ □ □ □ □ □ □ □
□ □ □ □ ■ ■ ■ □ □ ■ □ □ ■ □ □ ■ ■
□ □ □ □ ■ ■ □ □ □ ■ ■ □ □ □ ■ □ ■
□ □ □ □ □ □ □ □ ■ ■ □ ■ □ □ □ ■ □
□ □ □ ■ ■ □ □ □ ■ ■ ■ □ □ ■ □ □ □
□ □ ■ ■ ■ □ □ □ ■ ■ □ □ □ ■ ■ □ □
□ ■ □ ■ ■ □ □ □ □ □ □ □ ■ ■ □ ■ □
□ □ ■ ■ □ □ □ ■ ■ □ □ □ ■ ■ ■ □ □
□ □ □ ■ □ □ ■ ■ ■ □ □ □ ■ ■ □ □ □
□ ■ □ □ □ ■ □ ■ ■ □ □ □ □ □ □ □ □
□ ■ □ □ □ □ ■ □ □ □ □ □ □ □ □ □ □
世代: 2
□ □ □ □ ■ □ □ □ □ □ □ □ □ □ □ □ □
□ □ □ ■ □ □ ■ □ □ ■ ■ □ □ □ □ ■ ■
□ □ □ □ ■ □ ■ □ □ □ □ ■ □ □ ■ □ ■
□ □ □ ■ □ ■ □ □ □ □ □ ■ □ □ ■ ■ □
□ □ ■ □ ■ □ □ ■ □ □ □ □ ■ ■ □ □ □
□ □ □ □ □ ■ □ □ ■ □ ■ □ □ □ □ □ □
□ ■ □ □ □ □ □ ■ □ ■ □ □ □ □ □ ■ □
□ □ □ □ □ □ ■ □ ■ □ □ ■ □ □ □ □ □
□ □ □ ■ ■ □ □ □ □ ■ □ □ ■ □ ■ □ □
□ □ ■ □ □ ■ □ □ ■ □ □ □ □ □ □ □ □
□ □ □ □ □ □ ■ ■ □ □ □ □ □ □ □ □ □
世代: 3
□ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □
□ □ □ ■ ■ □ □ □ □ □ ■ □ □ □ □ ■ ■
□ □ □ ■ ■ □ ■ □ □ □ □ ■ □ □ ■ □ ■
□ □ □ ■ □ ■ ■ □ □ □ □ ■ □ □ ■ ■ □
□ □ □ ■ ■ ■ ■ □ □ □ □ ■ ■ ■ ■ □ □
□ □ □ □ □ □ ■ ■ ■ ■ □ □ □ □ □ □ □
□ □ □ □ □ □ ■ ■ □ ■ ■ □ □ □ □ □ □
□ □ □ □ □ □ □ ■ ■ ■ ■ □ □ □ □ □ □
□ □ □ ■ ■ ■ □ ■ ■ ■ □ □ □ □ □ □ □
□ □ □ ■ ■ ■ ■ ■ ■ □ □ □ □ □ □ □ □
□ □ □ □ □ □ ■ ■ □ □ □ □ □ □ □ □ □
世代: 4
□ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □
□ □ □ ■ ■ ■ □ □ □ □ □ □ □ □ □ ■ ■
□ □ ■ □ □ □ ■ □ □ □ ■ ■ □ □ ■ □ ■
□ □ ■ □ □ □ □ ■ □ □ ■ ■ □ □ □ □ □
□ □ □ ■ □ □ □ □ ■ □ ■ ■ ■ ■ ■ ■ □
□ □ □ □ ■ □ □ □ □ ■ □ ■ ■ ■ □ □ □
□ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □
□ □ □ □ ■ ■ □ □ □ □ □ □ □ □ □ □ □
□ □ □ ■ □ □ □ □ □ □ ■ □ □ □ □ □ □
□ □ □ ■ □ □ □ □ □ ■ □ □ □ □ □ □ □
□ □ □ □ ■ □ □ □ ■ □ □ □ □ □ □ □ □
世代: 5
□ □ □ □ ■ □ □ □ □ □ □ □ □ □ □ □ □
□ □ □ ■ ■ ■ □ □ □ □ □ □ □ □ □ ■ ■
□ □ ■ □ ■ ■ ■ □ □ □ ■ ■ □ □ □ □ ■
□ □ ■ ■ □ □ □ ■ □ □ □ □ □ □ □ □ □
□ □ □ ■ □ □ □ □ ■ □ □ □ □ □ ■ □ □
□ □ □ □ □ □ □ □ □ ■ □ □ □ □ □ □ □
□ □ □ □ ■ ■ □ □ □ □ □ □ ■ □ □ □ □
□ □ □ □ ■ □ □ □ □ □ □ □ □ □ □ □ □
□ □ □ ■ □ □ □ □ □ □ □ □ □ □ □ □ □
□ □ □ ■ ■ □ □ □ □ ■ □ □ □ □ □ □ □
□ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □
世代: 6
□ □ □ ■ ■ ■ □ □ □ □ □ □ □ □ □ □ □
□ □ □ □ □ □ ■ □ □ □ □ □ □ □ □ ■ ■
□ □ ■ □ □ □ ■ □ □ □ □ □ □ □ □ ■ ■
□ □ ■ □ □ ■ ■ ■ □ □ □ □ □ □ □ □ □
□ □ ■ ■ □ □ □ □ ■ □ □ □ □ □ □ □ □
□ □ □ □ ■ □ □ □ □ □ □ □ □ □ □ □ □
□ □ □ □ ■ ■ □ □ □ □ □ □ □ □ □ □ □
□ □ □ ■ ■ ■ □ □ □ □ □ □ □ □ □ □ □
□ □ □ ■ □ □ □ □ □ □ □ □ □ □ □ □ □
□ □ □ ■ ■ □ □ □ □ □ □ □ □ □ □ □ □
□ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □
世代: 7
□ □ □ □ ■ ■ □ □ □ □ □ □ □ □ □ □ □
□ □ □ ■ ■ □ ■ □ □ □ □ □ □ □ □ ■ ■
□ □ □ □ □ □ □ □ □ □ □ □ □ □ □ ■ ■
□ ■ ■ □ □ ■ ■ ■ □ □ □ □ □ □ □ □ □
□ □ ■ ■ ■ ■ ■ ■ □ □ □ □ □ □ □ □ □
□ □ □ □ ■ ■ □ □ □ □ □ □ □ □ □ □ □
□ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □
□ □ □ ■ □ ■ □ □ □ □ □ □ □ □ □ □ □
□ □ ■ □ □ ■ □ □ □ □ □ □ □ □ □ □ □
□ □ □ ■ ■ □ □ □ □ □ □ □ □ □ □ □ □
□ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □
世代: 8
□ □ □ ■ ■ ■ □ □ □ □ □ □ □ □ □ □ □
□ □ □ ■ ■ □ □ □ □ □ □ □ □ □ □ ■ ■
□ □ ■ ■ ■ □ □ ■ □ □ □ □ □ □ □ ■ ■
□ ■ ■ □ □ □ □ ■ □ □ □ □ □ □ □ □ □
□ ■ ■ □ □ □ □ ■ □ □ □ □ □ □ □ □ □
□ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □
□ □ □ □ □ ■ □ □ □ □ □ □ □ □ □ □ □
□ □ □ □ ■ □ □ □ □ □ □ □ □ □ □ □ □
□ □ ■ □ □ ■ □ □ □ □ □ □ □ □ □ □ □
□ □ □ ■ ■ □ □ □ □ □ □ □ □ □ □ □ □
□ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □
世代: 9
□ □ □ ■ □ ■ □ □ □ □ □ □ □ □ □ □ □
□ □ □ □ □ □ □ □ □ □ □ □ □ □ □ ■ ■
□ ■ □ □ ■ □ □ □ □ □ □ □ □ □ □ ■ ■
□ □ □ □ □ □ ■ ■ ■ □ □ □ □ □ □ □ □
□ ■ ■ □ □ □ □ □ □ □ □ □ □ □ □ □ □
□ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □
□ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □
□ □ □ □ ■ ■ □ □ □ □ □ □ □ □ □ □ □
□ □ □ □ □ ■ □ □ □ □ □ □ □ □ □ □ □
□ □ □ ■ ■ □ □ □ □ □ □ □ □ □ □ □ □
□ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □
世代: 10
□ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □
□ □ □ □ ■ □ □ □ □ □ □ □ □ □ □ ■ ■
□ □ □ □ □ □ □ ■ □ □ □ □ □ □ □ ■ ■
□ ■ ■ □ □ □ □ ■ □ □ □ □ □ □ □ □ □
□ □ □ □ □ □ □ ■ □ □ □ □ □ □ □ □ □
□ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □
□ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □
□ □ □ □ ■ ■ □ □ □ □ □ □ □ □ □ □ □
□ □ □ ■ □ ■ □ □ □ □ □ □ □ □ □ □ □
□ □ □ □ ■ □ □ □ □ □ □ □ □ □ □ □ □
□ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □
今後の展開予定
- 初期状態を任意指定できるように
- ゲームロジックを並列処理で実行
- 他の言語でのチャレンジ
あとがき
未知のElixir言語との遭遇
Elixir言語との初めての出会いは、オンラインテックカンファレンスQiita Conference 2025 Autumnでの@piacerex様の公演、
【今のコンピュータはAIにもWebにも向いていないので作り直そう】
でした。「ノイマン型コンピュータは今の技術には向いていない」「今のコンピューティングは電力を消費し過ぎている」という問題提起と、「今こそ性能向上と省電力を両立できる技術の確立化とそれに向けての活動」にいたく感銘いたしました。
そして問題解決のための言語としてElixirが紹介されたことで存在を知り興味が湧きました。開発者経験は長い方ですが、まだ見ぬ言語が世界に溢れているのを痛感し、そこに習得への欲求が発言しました。まさに教えである「知らぬ者よ、かねて血を怖れたまえ」を深く刻む次第です。
おわりに
ここまで読んでいただきありがとうございます。
今後も思いつくままに記事投稿を続けて行きたい所存であります。
これも巡り合わせだ、共に壁越えといこうじゃないか。