第21章: ベストプラクティス¶
はじめに¶
関数型プログラミングのベストプラクティスを実践的な例とともに解説します。良いコードと改善が必要なコードを比較しながら学びます。
1. 不変性と純粋関数¶
悪い例: 外部状態への依存¶
# ❌ 純粋関数ではない
def get_current_time_greeting do
hour = DateTime.utc_now().hour
cond do
hour < 12 -> "Good morning"
hour < 18 -> "Good afternoon"
true -> "Good evening"
end
end
良い例: 依存性の注入¶
# ✅ 純粋関数
def greeting_for_hour(hour) when hour >= 0 and hour < 24 do
cond do
hour < 12 -> "Good morning"
hour < 18 -> "Good afternoon"
true -> "Good evening"
end
end
# 不純なラッパー
def current_greeting do
greeting_for_hour(DateTime.utc_now().hour)
end
不変データの操作¶
# マップの安全な更新
def update_user(user, updates) do
Map.merge(user, updates)
end
# ネストしたデータの更新
def update_nested(data, path, func) do
update_in(data, path, func)
end
# リストへの追加(先頭が効率的)
def prepend(list, item), do: [item | list]
2. パイプラインと関数合成¶
悪い例: ネストした関数呼び出し¶
# ❌ 読みにくい
def process(data) do
Enum.join(
Enum.map(
Enum.filter(
String.split(data, ","),
fn s -> String.length(String.trim(s)) > 0 end
),
&String.upcase(String.trim(&1))
),
" | "
)
end
良い例: パイプライン¶
# ✅ 読みやすい
def process(data) do
data
|> String.split(",")
|> Enum.map(&String.trim/1)
|> Enum.filter(&(String.length(&1) > 0))
|> Enum.map(&String.upcase/1)
|> Enum.join(" | ")
end
関数合成¶
# 複数の関数を合成
def pipe_compose(functions) do
fn x ->
Enum.reduce(functions, x, fn f, acc -> f.(acc) end)
end
end
# 使用例
pipeline = pipe_compose([
fn x -> x + 1 end,
fn x -> x * 2 end
])
pipeline.(5) # => 12
3. エラーハンドリング¶
タグ付きタプル¶
# 成功/失敗を明示的に返す
def divide(a, b) when b != 0, do: {:ok, a / b}
def divide(_a, 0), do: {:error, :division_by_zero}
# nilの代わりにタグ付きタプル
def find_user(users, id) do
case Enum.find(users, &(&1.id == id)) do
nil -> {:error, :not_found}
user -> {:ok, user}
end
end
with構文¶
def process_order(order_id, user_id, users, orders) do
with {:ok, user} <- find_user(users, user_id),
{:ok, order} <- find_order(orders, order_id),
:ok <- validate_owner(user, order) do
{:ok, %{user: user, order: order}}
end
end
Railway Oriented Programming¶
defmodule Railway do
def bind({:ok, value}, func), do: func.(value)
def bind({:error, _} = error, _func), do: error
def pipeline(value, functions) do
Enum.reduce(functions, {:ok, value}, fn func, acc ->
bind(acc, func)
end)
end
end
4. テスタビリティ¶
依存性注入¶
# HTTPクライアントを注入可能に
def fetch_user(user_id, http_client \\ &default_http_client/1) do
case http_client.("https://api.example.com/users/#{user_id}") do
{:ok, body} -> {:ok, parse_json(body)}
{:error, _} = error -> error
end
end
# 時計を注入可能に
def is_weekend?(clock \\ &DateTime.utc_now/0) do
day = clock.() |> Date.day_of_week()
day in [6, 7]
end
# テストでの使用
test "is_weekend? with mock clock" do
saturday = fn -> ~D[2024-01-06] end
assert is_weekend?(saturday)
end
プロパティベーステスト向けの設計¶
# 逆演算が存在する関数
def encode(string), do: Base.encode64(string)
def decode(encoded), do: Base.decode64!(encoded)
# 冪等な関数
def normalize_email(email) do
email |> String.downcase() |> String.trim()
end
5. パフォーマンス最適化¶
遅延評価¶
# 大きなデータセットにはStreamを使用
def process_large_file(path) do
File.stream!(path)
|> Stream.map(&String.trim/1)
|> Stream.filter(&(String.length(&1) > 0))
|> Enum.take(100)
end
# 無限ストリーム
def fibonacci_stream do
Stream.unfold({0, 1}, fn {a, b} -> {a, {b, a + b}} end)
end
末尾再帰¶
# ❌ 末尾再帰でない
def sum_bad([]), do: 0
def sum_bad([h | t]), do: h + sum_bad(t)
# ✅ 末尾再帰
def sum_good(list), do: do_sum(list, 0)
defp do_sum([], acc), do: acc
defp do_sum([h | t], acc), do: do_sum(t, acc + h)
データ構造の選択¶
# O(1)のメンバーシップ検査にはMapSetを使用
def member_check_efficient(items, item) do
set = MapSet.new(items)
MapSet.member?(set, item)
end
# キーによる高速ルックアップにはMapを使用
def build_index(items, key_fn) do
Map.new(items, fn item -> {key_fn.(item), item} end)
end
6. コード構成¶
小さな関数¶
def validate_email(email) do
email
|> check_format()
|> check_domain()
|> check_length()
end
defp check_format({:error, _} = error), do: error
defp check_format(email) do
if String.match?(email, ~r/^[\w.+-]+@[\w.-]+\.\w+$/) do
email
else
{:error, :invalid_format}
end
end
モジュールの責務分離¶
# データ定義
defmodule User do
defstruct [:id, :name, :email, :role]
end
# ビジネスロジック
defmodule UserService do
def can_access?(user, resource) do
user.role in allowed_roles(resource)
end
end
# フォーマット
defmodule UserFormatter do
def to_display_name(%User{name: name}), do: name
end
7. 実践的なパターン¶
オプションパターン¶
@default_options [timeout: 5000, retries: 3]
def fetch_data(url, opts \\ []) do
opts = Keyword.merge(@default_options, opts)
# ...
end
ビルダーパターン¶
defmodule Builder do
defstruct fields: %{}, validations: []
def new, do: %__MODULE__{}
def set(b, key, value), do: %{b | fields: Map.put(b.fields, key, value)}
def build(b), do: {:ok, b.fields}
end
メモ化¶
defmodule Memoization do
use Agent
def memoize(key, func) do
case Agent.get(__MODULE__, &Map.get(&1, key)) do
nil ->
result = func.()
Agent.update(__MODULE__, &Map.put(&1, key, result))
result
cached ->
cached
end
end
end
まとめ¶
| ベストプラクティス | 目的 |
|---|---|
| 純粋関数 | テスタビリティ、予測可能性 |
| パイプライン | 可読性、データフロー明確化 |
| タグ付きタプル | 明示的なエラーハンドリング |
| 依存性注入 | テスト容易性 |
| 遅延評価 | パフォーマンス |
| 末尾再帰 | メモリ効率 |
| 小さな関数 | 保守性、再利用性 |
これらのプラクティスを意識することで、より堅牢で保守しやすい Elixir コードが書けます。