変数・型・不変性
基礎Elixir の変数は再代入できない(バインディング)。ただし同名変数へのリバインドは許可される。= は代入ではなく「パターンマッチ演算子」である。
x = 42
x = 100 # OK: リバインド(新しいバインディング)
{a, b} = {1, 2} # パターンマッチで分割代入
^x = 100 # ピン演算子: 値を固定して照合Elixir の主な型: 整数・浮動小数点・論理値(true/false はアトム)・アトム(:name)・文字列(バイナリ)・リスト・タプル・マップ。
42 # integer
3.14 # float
true # atom(== :true)
:ok # atom
"hello" # binary string(UTF-8)
[1, 2, 3] # list(linked list)
{:ok, 42} # tuple
%{a: 1} # mapアトムは名前そのものが値であるリテラル。nil・true・false もアトム。関数の戻り値パターン({:ok, val}・{:error, reason})に多用される。
:ok
:error
:hello
nil == false # false(nil と false は別のアトム)
is_atom(:ok) # true
is_nil(nil) # true^x とすると変数をリバインドせず、現在の値でパターンマッチを行う。
x = 1
^x = 1 # OK
^x = 2 # ** (MatchError) no match of right hand side value: 2制御フロー
基礎case はパターンマッチベースの分岐。when ガードで条件を追加できる。
result = {:ok, 42}
case result do
{:ok, value} when value > 0 -> "positive: #{value}"
{:ok, value} -> "non-positive: #{value}"
{:error, reason} -> "error: #{reason}"
endcond は最初の真条件の節を実行する(elif の連鎖に相当)。if/unless は単純な真偽分岐。
score = 75
cond do
score >= 90 -> "A"
score >= 70 -> "B"
true -> "C" # デフォルト節
end
if score >= 60, do: "Pass", else: "Fail"with はパターンマッチのチェーン。全て成功した場合のみ do ブロックを実行し、どれかが失敗すると else に流れる。ハッピーパスの記述に使う。
with {:ok, user} <- find_user(id),
{:ok, token} <- generate_token(user) do
{:ok, token}
else
{:error, reason} -> {:error, reason}
endfor で生成器・フィルタ・変換を組み合わせたコレクション生成。into: で結果のデータ構造を指定できる。
evens = for n <- 1..10, rem(n, 2) == 0, do: n * n
# [4, 16, 36, 64, 100]
pairs = for x <- [1,2], y <- [:a,:b], do: {x, y}
# [{1,:a},{1,:b},{2,:a},{2,:b}]関数
基礎def はパブリック関数、defp はプライベート関数(同モジュール内のみ)。関数は必ずモジュール内で定義する。
defmodule Math do
def add(a, b), do: a + b # パブリック
defp square(n), do: n * n # プライベート
def square_sum(a, b), do: square(a) + square(b)
end
Math.add(3, 4) # 7同名・同アリティの関数を複数定義でき、引数のパターンマッチで節が選択される。再帰と組み合わせて使う。
defmodule Fib do
def calc(0), do: 0
def calc(1), do: 1
def calc(n) when n > 1, do: calc(n-1) + calc(n-2)
end
Fib.calc(10) # 55fn ... -> ... end で無名関数を定義。& キャプチャ構文で短く書ける。.(...) で呼び出す。
double = fn x -> x * 2 end
double.(5) # 10
double2 = &(&1 * 2) # キャプチャ構文
double2.(5) # 10
add = &(&1 + &2)
add.(3, 4) # 7|> で関数を左から右へチェーン。左辺の値が次の関数の第一引数になる。読みやすい変換パイプラインを構築できる。
" hello world "
|> String.trim()
|> String.split(" ")
|> Enum.map(&String.capitalize/1)
|> Enum.join(" ")
# "Hello World"コレクション
基礎連結リスト。先頭への追加が O(1)、末尾は O(n)。| でパターン分割。Enum モジュールで操作。
list = [1, 2, 3]
[h | t] = list # h=1, t=[2,3]
new_list = [0 | list] # [0,1,2,3]
Enum.map(list, &(&1 * 2)) # [2,4,6]
Enum.filter(list, &(&1 > 1)) # [2,3]
Enum.reduce(list, 0, &+/2) # 6固定長・高速アクセスのデータ構造。関数の戻り値に {:ok, val} パターンで使われる。要素の変更は新しいタプルを生成する。
t = {:ok, "hello", 42}
elem(t, 1) # "hello"
put_elem(t, 2, 99) # {:ok, "hello", 99}
tuple_size(t) # 3キーバリューストア。アトムキーには map.key でアクセス可能。Map.put・Map.update で変更(新しい Map を返す)。
m = %{name: "Alice", age: 30}
m.name # "Alice"
Map.get(m, :age, 0) # 30
m2 = Map.put(m, :age, 31) # 新しい Map
m3 = %{m | age: 31} # 更新構文(既存キーのみ)[key: value] 形式のタプルリスト。順序を保持し、重複キーも持てる。関数オプションや DSL によく使われる。
opts = [timeout: 5000, retry: 3]
Keyword.get(opts, :timeout) # 5000
# 関数に渡す場合(末尾の [] は省略可)
String.split("a,b,c", ",", trim: true)文字列・バイナリ
基礎Elixir の文字列は UTF-8 エンコードのバイナリ。"..." で文字列補間(#{expr})が使える。~s シギルや三重クォート(""")もある。
name = "Elixir"
"Hello, #{name}!" # 文字列補間
String.length("hello") # 5
String.upcase("hello") # "HELLO"
String.split("a,b", ",") # ["a","b"]シングルクォートの '...' はチャーリスト(コードポイントのリスト)。シギル(~r・~w・~c 等)で特殊なリテラルを表現できる。
'hello' # チャーリスト([104,101,108,108,111])
~w[foo bar baz] # ["foo","bar","baz"]
~r/elixir/i # 正規表現
~c[hello] # チャーリスト(新記法)<<>> でバイナリのビット/バイトレベルのパターンマッチが可能。ネットワークプロトコルやファイルパーサーに使われる。
<<r::8, g::8, b::8>> = <<255, 128, 0>>
# r=255, g=128, b=0
<<first::binary-size(3), rest::binary>> = "Hello"
# first="Hel", rest="lo"エラー処理
基礎Elixir の慣習的なエラーハンドリング。関数は {:ok, value} または {:error, reason} を返し、呼び出し元がパターンマッチで処理する。
case File.read("data.txt") do
{:ok, content} -> process(content)
{:error, :enoent} -> IO.puts("File not found")
{:error, reason} -> IO.puts("Error: #{reason}")
end例外(Exception)と throw の両方を捕捉できる。rescue は例外、catch は throw/exit/:EXIT。通常は {:ok, _} パターンを優先する。
try do
String.to_integer("abc")
rescue
e in ArgumentError -> "Invalid: #{e.message}"
after
IO.puts("cleanup") # 必ず実行
endfunc! の命名規則の関数は失敗時に例外を発生させる。成功値を直接返すため、パターンマッチが不要な場面で使う。
# 通常版({:ok, _} / {:error, _} を返す)
{:ok, content} = File.read("data.txt")
# バン版(成功時は内容を返し、失敗は例外)
content = File.read!("data.txt")プロセスとメッセージパッシング
基礎Elixir のプロセスは BEAM VM 上の軽量プロセス(スレッドではない)。数百万個同時に起動できる。spawn/1 で新プロセスを起動し PID で識別。
pid = spawn(fn -> IO.puts("Hello from process!") end)
Process.alive?(pid) # false(すぐ終了)
self() # 現在のプロセスの PID
Process.info(self(), :memory)プロセス間通信はメッセージパッシングのみ。send/2 でメッセージ送信、receive でメールボックスから取り出す。
parent = self()
pid = spawn(fn ->
receive do
{:hello, from} -> send(from, {:reply, "Hi!"})
end
end)
send(pid, {:hello, parent})
receive do
{:reply, msg} -> IO.puts(msg) # "Hi!"
endspawn_link/1 でリンクしたプロセスは一方がクラッシュするともう一方も終了(フォールトトレランス)。spawn_monitor/1 はクラッシュを通知として受け取れる。
# モニター(片方向の監視)
{pid, ref} = spawn_monitor(fn -> raise "oops" end)
receive do
{:DOWN, ^ref, :process, _pid, reason} ->
IO.puts("Died: #{inspect(reason)}")
endAgent は状態を保持するプロセスの抽象。Task は非同期タスクの実行と結果取得(Task.async・Task.await)のための高レベル API。
# Task で並行処理
t1 = Task.async(fn -> :timer.sleep(100); 1 end)
t2 = Task.async(fn -> :timer.sleep(100); 2 end)
[Task.await(t1), Task.await(t2)] # [1, 2]
# Agent で状態管理
{:ok, agent} = Agent.start_link(fn -> 0 end)
Agent.update(agent, &(&1 + 1))
Agent.get(agent, & &1) # 1GenServer・OTP
基礎GenServer は OTP の汎用サーバービヘイビア。call(同期)と cast(非同期)でメッセージ送受信し、handle_call/handle_cast でコールバック処理する。
defmodule Counter do
use GenServer
def start_link(init), do: GenServer.start_link(__MODULE__, init, name: __MODULE__)
def increment, do: GenServer.cast(__MODULE__, :inc)
def value, do: GenServer.call(__MODULE__, :get)
@impl true
def init(n), do: {:ok, n}
@impl true
def handle_cast(:inc, n), do: {:noreply, n + 1}
@impl true
def handle_call(:get, _from, n), do: {:reply, n, n}
endSupervisor は子プロセスの起動・再起動を管理。strategy: :one_for_one(1つがクラッシュしたら1つ再起動)などで障害耐性を設計する。
defmodule MyApp.Supervisor do
use Supervisor
def start_link(_), do: Supervisor.start_link(__MODULE__, :ok, name: __MODULE__)
@impl true
def init(:ok) do
children = [
{Counter, 0},
{MyWorker, []}
]
Supervisor.init(children, strategy: :one_for_one)
end
endOTP アプリケーションは起動時にスーパービジョンツリーを立ち上げる。mix.exs でアプリケーション設定し、Application.start/2 をコールバックに実装する。
defmodule MyApp.Application do
use Application
@impl true
def start(_type, _args) do
children = [MyApp.Supervisor]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
endPhoenix フレームワーク概要
応用Elixir 最大の Web フレームワーク。高並行性・低レイテンシが特徴。MVC + Channel(WebSocket)+ LiveView(リアルタイム UI)を提供。
# mix.exs
{:phoenix, "~> 1.7"},
{:phoenix_live_view, "~> 0.20"}
# $ mix phx.new my_app
# $ cd my_app && mix phx.serverRouter でパスを定義 → Controller でリクエスト処理 → View でレスポンス生成。Plug はミドルウェアの仕組みで、パイプラインを構成できる。
# router.ex
scope "/api", MyAppWeb do
pipe_through :api
get "/users", UserController, :index
post "/users", UserController, :create
end
# user_controller.ex
def index(conn, _params) do
users = Accounts.list_users()
json(conn, users)
endEcto は ORM ではなくデータマッピングとクエリビルダー。Schema・Changeset(バリデーション)・Repo(DB 操作)・クエリ DSL を提供。
defmodule User do
use Ecto.Schema
schema "users" do
field :name, :string
field :email, :string
timestamps()
end
def changeset(user, attrs) do
user |> cast(attrs, [:name, :email])
|> validate_required([:name, :email])
end
end
Repo.all(from u in User, where: u.name == "Alice")列挙・Stream
基礎Enum はコレクション操作の標準ライブラリ。即時評価(全要素処理してから次へ)。
Enum.map([1,2,3], &(&1 * 2)) # [2,4,6]
Enum.filter([1,2,3,4], &rem(&1,2)==0) # [2,4]
Enum.reduce([1,2,3], 0, &+/2) # 6
Enum.sort_by(users, & &1.name)
Enum.group_by(users, & &1.role)Stream は遅延評価版の Enum。大量データや無限シーケンスの効率的な処理に使う。最終的に Enum.to_list などで評価する。
1..1_000_000
|> Stream.filter(&rem(&1, 2) == 0)
|> Stream.map(&(&1 * &1))
|> Enum.take(5)
# [4, 16, 36, 64, 100](必要な分だけ計算)into: でマップに集約したり、reduce: で任意の初期値に畳み込める。
# Map を生成
word_lengths = for word <- ~w[hello world], into: %{} do
{word, String.length(word)}
end
# %{"hello" => 5, "world" => 5}モジュール・メタプログラミング
応用モジュールはコードの名前空間。@moduledoc・@doc でドキュメント、@spec で型仕様を記述。@ アトリビュートはコンパイル時定数としても使える。
defmodule MyMath do
@moduledoc "数学ユーティリティ"
@pi 3.14159265
@spec circle_area(number) :: float
@doc "円の面積を計算"
def circle_area(r), do: @pi * r * r
enddefmacro でコンパイル時にコードを生成するマクロを定義。quote/unquote で AST(抽象構文木)を操作する。
defmodule MyMacro do
defmacro unless(condition, do: body) do
quote do
if !unquote(condition), do: unquote(body)
end
end
end
import MyMacro
unless false, do: IO.puts("executed!")ビヘイビアはモジュールが実装すべきコールバック関数の仕様を定義する仕組み(インターフェースに相当)。OTP の GenServer もビヘイビア。
defmodule Storage do
@callback save(key :: String.t, value :: term) :: :ok | {:error, term}
@callback get(key :: String.t) :: {:ok, term} | :error
end
defmodule MemStorage do
@behaviour Storage
@impl true
def save(k, v), do: :ets.insert(:store, {k, v}); :ok
@impl true
def get(k), do: case :ets.lookup(:store, k) do
[{^k, v}] -> {:ok, v}; [] -> :error
end
endファイル・IO
基礎File モジュールでファイルの読み書き。File.stream! で大きなファイルを効率的にストリーム処理できる。
File.write!("hello.txt", "Hello, World!")
content = File.read!("hello.txt") # "Hello, World!"
File.stream!("large.csv")
|> Stream.map(&String.trim/1)
|> Enum.each(&process/1)標準入出力。IO.puts(改行あり出力)・IO.write(改行なし)・IO.gets(入力)・IO.inspect(デバッグ表示)。
IO.puts("Hello!")
IO.write("no newline")
name = IO.gets("Name: ") |> String.trim()
# パイプライン内デバッグに便利
[1, 2, 3]
|> IO.inspect(label: "before")
|> Enum.map(&(&1 * 2))
|> IO.inspect(label: "after")ファイルパス操作のユーティリティ。OS を意識せずクロスプラットフォームなパス操作ができる。
Path.join(["home", "user", "file.txt"]) # "home/user/file.txt"
Path.expand("~/data") # "/Users/user/data"
Path.extname("app.ex") # ".ex"
Path.dirname("/home/user/file.txt") # "/home/user"型仕様・Dialyzer
応用@spec で関数の型仕様を記述。Dialyzer(静的解析ツール)が型不整合を検出する。Elixir は動的型付けだが型仕様でドキュメント化と解析が可能。
@type user :: %{name: String.t(), age: non_neg_integer()}
@spec greet(String.t()) :: String.t()
def greet(name), do: "Hello, #{name}!"
@spec divide(number(), number()) :: {:ok, float()} | {:error, :division_by_zero}
def divide(_, 0), do: {:error, :division_by_zero}
def divide(a, b), do: {:ok, a / b}dialyxir は Mix プロジェクトで Dialyzer を使いやすくするツール。PLT(Persistent Lookup Table)のビルドに時間がかかるが、型エラーを事前に検出できる。
# mix.exs
{:dialyxir, "~> 1.0", only: [:dev], runtime: false}
# $ mix dialyzer # PLT 構築 + 解析ガード節(when)で使える式は限定的(副作用なし)。is_integer・is_list・算術・比較・and/or/not など Kernel モジュールの関数が使える。
def classify(x) when is_integer(x) and x > 0, do: :positive
def classify(x) when is_integer(x) and x < 0, do: :negative
def classify(0), do: :zero
def classify(_), do: :not_a_numberMix・エコシステム
基礎mix は Elixir 標準のビルドツール。プロジェクト作成・コンパイル・テスト・依存管理を一元化。
# 新規プロジェクト
mix new my_app
mix new my_app --sup # スーパービジョンツリー付き
# よく使うコマンド
mix compile # コンパイル
mix test # テスト
mix format # フォーマット
mix deps.get # 依存解決hex.pm は Elixir/Erlang のパッケージリポジトリ。mix.exs の deps/0 で依存を宣言し mix deps.get で取得。
# mix.exs
defp deps do
[
{:phoenix, "~> 1.7"},
{:ecto_sql, "~> 3.10"},
{:jason, "~> 1.4"},
{:ex_doc, "~> 0.30", only: :dev}
]
end組み込みのテストフレームワーク。assert・refute・assert_raise でアサーション。describe・setup でテストの構造化。
defmodule MathTest do
use ExUnit.Case, async: true
describe "add/2" do
test "adds two numbers" do
assert Math.add(1, 2) == 3
end
test "handles negatives" do
assert Math.add(-1, 1) == 0
end
end
end分散処理・クラスタリング
応用BEAM VM は分散ノード間の透過的なメッセージパッシングをサポート。Node.connect/1 でノードを接続し、他ノードのプロセスに send できる。
# ノード起動: iex --sname node1@localhost
Node.connect(:"node2@localhost")
Node.list() # 接続ノード一覧
# 他ノードにメッセージ送信
send({:my_process, :"node2@localhost"}, {:hello, self()})libcluster はノード自動検出ライブラリ。Horde は分散スーパービジョンとレジストリを提供し、クラスター全体でのプロセス管理を可能にする。
# libcluster 設定例
config :libcluster,
topologies: [
k8s: [
strategy: Cluster.Strategy.Kubernetes,
config: [kubernetes_selector: "app=my_app"]
]
]ETS(Erlang Term Storage)は高速なインメモリストレージ(テーブル)。Mnesia は分散インメモリデータベース。どちらも Erlang/OTP の標準機能。
:ets.new(:cache, [:named_table, :public])
:ets.insert(:cache, {"key", "value"})
:ets.lookup(:cache, "key") # [{"key","value"}]
:ets.delete(:cache, "key")パフォーマンス・デバッグ
応用:observer.start() で BEAM の GUI モニタリングツールを起動。プロセスツリー・メモリ・メッセージキューをリアルタイム観察できる。
# iex で起動
:observer.start()
# プロセス情報
Process.info(pid)
Process.list() |> length() # 全プロセス数
# メッセージキュー確認
Process.info(pid, :message_queue_len)benchee は Elixir のベンチマークライブラリ。複数の実装を比較し、ops/sec・メモリ使用量・分布を計測できる。
Benchee.run(%{
"Enum.map" => fn -> Enum.map(1..1000, &(&1 * 2)) end,
"for" => fn -> for n <- 1..1000, do: n * 2 end
})recon は本番環境のデバッグツールセット。recon_trace でトレーシング、recon:proc_count でプロセスのリソース消費ランキングを確認できる。
# 本番での高いメモリ使用プロセス調査
:recon.proc_count(:memory, 10)
# 関数呼び出しトレース
:recon_trace.calls({MyModule, :my_fun, :_}, 100)文字列の繰り返し連結には IO List を使う。大量のデータは Stream で遅延処理。末尾再帰で深い再帰を最適化。
# 悪い例: 文字列を都度連結(O(n^2))
Enum.reduce(list, "", fn s, acc -> acc <> s end)
# 良い例: IO リストで最後に join(O(n))
IO.iodata_to_binary(Enum.intersperse(list, ""))
# または
Enum.join(list, "")