MeWrite Docs

Elixir: Protocol.UndefinedError

Elixirでプロトコルが型に対して実装されていない場合のエラー

概要

Elixirでプロトコルを呼び出した際に、その型に対するプロトコルの実装が存在しない場合に発生するエラーです。

エラーメッセージ

** (Protocol.UndefinedError) protocol Enumerable not implemented for %MyStruct{} of type MyStruct (a struct)

または

** (Protocol.UndefinedError) protocol String.Chars not implemented for %{key: "value"} of type Map

原因

  1. プロトコル未実装: カスタム型にプロトコルを実装していない
  2. 型の誤用: プロトコルがサポートしていない型を渡した
  3. deriving 忘れ: @deriveでプロトコルを導出していない

解決策

1. defimpl でプロトコル実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
defmodule User do
  defstruct [:id, :name, :email]
end

# String.Chars プロトコルを実装
defimpl String.Chars, for: User do
  def to_string(%User{name: name, email: email}) do
    "#{name} <#{email}>"
  end
end

# 使用
user = %User{id: 1, name: "John", email: "john@example.com"}
"#{user}"  # => "John <john@example.com>"

2. @derive でプロトコル導出

1
2
3
4
5
defmodule User do
  @derive {Inspect, only: [:name, :email]}  # :id を隠す
  @derive Jason.Encoder  # JSON エンコード可能に
  defstruct [:id, :name, :email]
end

3. Enumerable プロトコルの実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
defmodule CustomRange do
  defstruct [:start, :stop]
end

defimpl Enumerable, for: CustomRange do
  def count(%CustomRange{start: start, stop: stop}) do
    {:ok, stop - start + 1}
  end

  def member?(%CustomRange{start: start, stop: stop}, value) do
    {:ok, value >= start and value <= stop}
  end

  def reduce(%CustomRange{start: start, stop: stop}, acc, fun) do
    reduce_range(start, stop, acc, fun)
  end

  defp reduce_range(current, stop, {:cont, acc}, fun) when current <= stop do
    reduce_range(current + 1, stop, fun.(current, acc), fun)
  end
  defp reduce_range(_, _, {:halt, acc}, _fun), do: {:halted, acc}
  defp reduce_range(_, _, {:suspend, acc}, fun), do: {:suspended, acc, &reduce_range(&1, &2, &3, fun)}
  defp reduce_range(_, _, acc, _fun), do: {:done, acc}

  def slice(_), do: {:error, __MODULE__}
end

# 使用
range = %CustomRange{start: 1, stop: 5}
Enum.to_list(range)  # => [1, 2, 3, 4, 5]

4. Jason.Encoder の実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
defmodule Event do
  defstruct [:id, :name, :timestamp]
end

defimpl Jason.Encoder, for: Event do
  def encode(%Event{id: id, name: name, timestamp: ts}, opts) do
    Jason.Encode.map(%{
      id: id,
      name: name,
      timestamp: DateTime.to_iso8601(ts)
    }, opts)
  end
end

5. Inspect プロトコルのカスタマイズ

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
defmodule Password do
  defstruct [:value]
end

defimpl Inspect, for: Password do
  def inspect(_password, _opts) do
    "#Password<***>"  # 値を隠す
  end
end

# 使用
password = %Password{value: "secret123"}
inspect(password)  # => "#Password<***>"

6. Any プロトコルへのフォールバック

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
defprotocol Describable do
  @fallback_to_any true
  def describe(value)
end

defimpl Describable, for: Any do
  def describe(value) do
    "Unknown: #{inspect(value)}"
  end
end

defimpl Describable, for: User do
  def describe(%User{name: name}) do
    "User: #{name}"
  end
end

7. 型に応じた変換

1
2
3
4
5
6
7
8
9
# マップを文字列に変換したい場合
def map_to_string(map) when is_map(map) do
  map
  |> Enum.map(fn {k, v} -> "#{k}: #{v}" end)
  |> Enum.join(", ")
end

# または Kernel.inspect/1 を使用
inspect(%{key: "value"})  # => "%{key: \"value\"}"

よくある間違い

  • structにEnumerableを実装せずEnumを使おうとする
  • JSONエンコード時にDateTimeをそのまま渡す
  • Mapに対してString.Charsを期待する
  • @derive の位置が defstruct の後になっている

Elixir の他のエラー

最終更新: 2025-12-09