第3章: 多態性の実現方法¶
はじめに¶
多態性(ポリモーフィズム)は、同じインターフェースで異なる振る舞いを実現する強力な概念です。Elixir では、パターンマッチング、プロトコル、ビヘイビアという複数のメカニズムで多態性を実現します。
本章では、これらのメカニズムを使い分けて、柔軟で拡張性の高いコードを書く方法を学びます。
1. パターンマッチングによる多態性¶
Elixir ではタグ付きタプルと関数のパターンマッチングを使って、代数的データ型(ADT)に相当する設計を実現できます。
基本的な使い方¶
# 図形をタグ付きタプルで表現
@type shape ::
{:rectangle, width :: number(), height :: number()}
| {:circle, radius :: number()}
| {:triangle, base :: number(), height :: number()}
# 図形の面積を計算する
def calculate_area({:rectangle, width, height}), do: width * height
def calculate_area({:circle, radius}), do: :math.pi() * radius * radius
def calculate_area({:triangle, base, height}), do: base * height / 2
# 使用例
calculate_area({:rectangle, 4, 5}) # => 20
calculate_area({:circle, 3}) # => 28.27...
calculate_area({:triangle, 6, 5}) # => 15.0
構造体を使った多態性¶
defmodule Rectangle do
defstruct [:width, :height]
end
defmodule Circle do
defstruct [:radius]
end
defmodule Triangle do
defstruct [:base, :height]
end
def area(%Rectangle{width: w, height: h}), do: w * h
def area(%Circle{radius: r}), do: :math.pi() * r * r
def area(%Triangle{base: b, height: h}), do: b * h / 2
# 使用例
area(%Rectangle{width: 4, height: 5}) # => 20
area(%Circle{radius: 3}) # => 28.27...
2. 複合ディスパッチ¶
タプルを使ったパターンマッチングで、複数の値に基づくディスパッチを実現できます。
defmodule Payment do
defstruct [:method, :currency, :amount]
end
defmodule PaymentResult do
defstruct [:status, :message, :amount, :converted]
end
def process_payment(%Payment{method: method, currency: currency, amount: amount}) do
case {method, currency} do
{:credit_card, :jpy} ->
%PaymentResult{
status: "processed",
message: "クレジットカード(円)で処理しました",
amount: amount
}
{:credit_card, :usd} ->
%PaymentResult{
status: "processed",
message: "Credit card (USD) processed",
amount: amount,
converted: amount * 150
}
{:bank_transfer, :jpy} ->
%PaymentResult{
status: "pending",
message: "銀行振込を受け付けました",
amount: amount
}
_ ->
%PaymentResult{
status: "error",
message: "サポートされていない支払い方法です",
amount: amount
}
end
end
# 使用例
process_payment(%Payment{method: :credit_card, currency: :jpy, amount: 1000})
# => %PaymentResult{status: "processed", message: "クレジットカード(円)で処理しました", ...}
3. プロトコルによる多態性¶
プロトコルは、特定の操作セットを定義するインターフェースです。異なる型に対して同じ関数を呼び出せるようになります。
プロトコルの定義と実装¶
defprotocol Describable do
@doc "オブジェクトの説明を返す"
@spec describe(t) :: String.t()
def describe(value)
end
defmodule Product do
defstruct [:name, :price]
end
defmodule Service do
defstruct [:name, :hourly_rate]
end
defimpl Describable, for: Product do
def describe(%Product{name: name, price: price}) do
"商品: #{name} (#{price}円)"
end
end
defimpl Describable, for: Service do
def describe(%Service{name: name, hourly_rate: rate}) do
"サービス: #{name} (時給#{rate}円)"
end
end
# 組み込み型にも実装できる
defimpl Describable, for: Map do
def describe(map) do
"マップ with #{map_size(map)} keys"
end
end
defimpl Describable, for: List do
def describe(list) do
"リスト with #{length(list)} elements"
end
end
# 使用例
Describable.describe(%Product{name: "りんご", price: 150})
# => "商品: りんご (150円)"
Describable.describe(%{a: 1, b: 2, c: 3})
# => "マップ with 3 keys"
プロトコルの利点¶
- 拡張性: 既存の型に対して後から振る舞いを追加可能
- 分離: 型定義とプロトコル実装を別ファイルに分離可能
- 多態性: 異なる型に対して統一的なインターフェースを提供
4. ビヘイビアによる多態性¶
ビヘイビアは、モジュールが実装すべきコールバック関数を定義します。Java のインターフェースに似ています。
defmodule Serializer do
@callback serialize(data :: any()) :: String.t()
@callback deserialize(string :: String.t()) :: any()
end
defmodule JsonSerializer do
@behaviour Serializer
@impl Serializer
def serialize(data) do
# JSON シリアライズ実装
inspect(data)
end
@impl Serializer
def deserialize(_string) do
%{}
end
end
defmodule CsvSerializer do
@behaviour Serializer
@impl Serializer
def serialize(data) when is_list(data) do
data
|> Enum.map(&Enum.join(&1, ","))
|> Enum.join("\n")
end
def serialize(_), do: ""
@impl Serializer
def deserialize(string) do
string
|> String.split("\n")
|> Enum.map(&String.split(&1, ","))
end
end
# 使用例
CsvSerializer.serialize([["a", "b", "c"], ["1", "2", "3"]])
# => "a,b,c\n1,2,3"
ビヘイビアとプロトコルの使い分け¶
| 特性 | プロトコル | ビヘイビア |
|---|---|---|
| ディスパッチ | データの型による | モジュールによる |
| 用途 | 異なる型に同じ操作 | 交換可能な実装 |
| 例 | Enumerable, String.Chars |
GenServer, Supervisor |
5. 型クラス相当(プロトコル + 実装)¶
プロトコルを使って、関数型言語の型クラスに相当するパターンを実現できます。
defprotocol Monoid do
@doc "結合演算"
@spec combine(t, t) :: t
def combine(a, b)
end
defmodule Sum do
defstruct [:value]
def empty, do: %Sum{value: 0}
end
defmodule Multiply do
defstruct [:value]
def empty, do: %Multiply{value: 1}
end
defimpl Monoid, for: Sum do
def combine(%Sum{value: a}, %Sum{value: b}), do: %Sum{value: a + b}
end
defimpl Monoid, for: Multiply do
def combine(%Multiply{value: a}, %Multiply{value: b}), do: %Multiply{value: a * b}
end
defimpl Monoid, for: List do
def combine(a, b), do: a ++ b
end
# モノイドのリストを畳み込む
def fold_monoid(list, empty) do
Enum.reduce(list, empty, &Monoid.combine(&2, &1))
end
# 使用例
sums = [%Sum{value: 1}, %Sum{value: 2}, %Sum{value: 3}]
fold_monoid(sums, Sum.empty())
# => %Sum{value: 6}
6. 動的ディスパッチ¶
マップを使った関数のディスパッチで、実行時に柔軟な振る舞いの切り替えができます。
def dispatch(handlers, operation, args) do
case Map.get(handlers, operation) do
nil -> raise ArgumentError, "Unknown operation: #{operation}"
handler -> apply(handler, args)
end
end
def create_calculator do
handlers = %{
add: fn a, b -> a + b end,
subtract: fn a, b -> a - b end,
multiply: fn a, b -> a * b end,
divide: fn a, b -> a / b end
}
fn operation, a, b ->
dispatch(handlers, operation, [a, b])
end
end
# 使用例
calc = create_calculator()
calc.(:add, 5, 3) # => 8
calc.(:divide, 10, 2) # => 5.0
7. 式の評価(再帰的なパターンマッチング)¶
パターンマッチングを使った再帰的なデータ構造の処理は、Elixir の得意分野です。
@type expr ::
{:num, number()}
| {:add, expr(), expr()}
| {:sub, expr(), expr()}
| {:mul, expr(), expr()}
| {:div, expr(), expr()}
def evaluate({:num, n}), do: n
def evaluate({:add, left, right}), do: evaluate(left) + evaluate(right)
def evaluate({:sub, left, right}), do: evaluate(left) - evaluate(right)
def evaluate({:mul, left, right}), do: evaluate(left) * evaluate(right)
def evaluate({:div, left, right}), do: evaluate(left) / evaluate(right)
def expr_to_string({:num, n}), do: to_string(n)
def expr_to_string({:add, left, right}) do
"(#{expr_to_string(left)} + #{expr_to_string(right)})"
end
# ... 他の演算子も同様
# 使用例
expr = {:add, {:num, 5}, {:mul, {:num, 2}, {:num, 3}}}
evaluate(expr) # => 11
expr_to_string(expr) # => "(5 + (2 * 3))"
8. ガード節による多態性¶
ガード節を使うと、型や値の条件に基づいて関数をディスパッチできます。
def describe_value(n) when is_integer(n) and n > 0, do: "正の整数: #{n}"
def describe_value(n) when is_integer(n) and n < 0, do: "負の整数: #{n}"
def describe_value(0), do: "ゼロ"
def describe_value(n) when is_float(n), do: "浮動小数点数: #{n}"
def describe_value(s) when is_binary(s), do: "文字列: #{s}"
def describe_value(list) when is_list(list), do: "リスト(要素数: #{length(list)})"
def describe_value(map) when is_map(map), do: "マップ(キー数: #{map_size(map)})"
def describe_value(_), do: "その他"
# 使用例
describe_value(42) # => "正の整数: 42"
describe_value(-5) # => "負の整数: -5"
describe_value(3.14) # => "浮動小数点数: 3.14"
describe_value("hello") # => "文字列: hello"
まとめ¶
本章では、Elixir における多態性の実現方法について学びました:
- パターンマッチング: タグ付きタプルと関数のマルチヘッドによる ADT
- 構造体: 明示的な型と構造体によるパターンマッチング
- 複合ディスパッチ: 複数の値に基づく分岐
- プロトコル: 型に対する振る舞いの定義と実装
- ビヘイビア: モジュールレベルのインターフェース定義
- 動的ディスパッチ: マップによる関数の実行時切り替え
- ガード節: 型や値の条件による分岐
これらのメカニズムを適切に使い分けることで、柔軟で保守しやすいコードを書くことができます。
参考コード¶
本章のコード例は以下のファイルで確認できます:
- ソースコード:
apps/elixir/part1/lib/chapter03.ex - テストコード:
apps/elixir/part1/test/chapter03_test.exs
次章予告¶
次章では、データ検証について学びます。Elixir での型チェックとバリデーションパターンを探ります。