Elixir The Enumerable Protocol
The Enumerable protocol is the set of functions that lets a value work with Enum. Giving a custom struct an Enumerable implementation makes Enum functions work on it.
An Enumerable implementation lets Enum list a struct
The Bag struct holds items in a list. Its reduce walks those items, so Enum.to_list works on a Bag.
defmodule Bag do
defstruct items: []
end
defimpl Enumerable, for: Bag do
def reduce(_, {:halt, acc}, _fun), do: {:halted, acc}
def reduce(%Bag{items: []}, {:cont, acc}, _fun), do: {:done, acc}
def reduce(%Bag{items: [head | tail]}, {:cont, acc}, fun) do
reduce(%Bag{items: tail}, fun.(head, acc), fun)
end
def reduce(%Bag{} = bag, {:suspend, acc}, fun), do: {:suspended, acc, &reduce(bag, &1, fun)}
def count(%Bag{items: items}), do: {:ok, length(items)}
def member?(%Bag{items: items}, value), do: {:ok, value in items}
def slice(%Bag{}), do: {:error, __MODULE__}
end
bag = struct(Bag, items: [10, 20, 30])
IO.inspect(Enum.to_list(bag))Count and membership work through the same implementation
Enum.count uses count, and Enum.member? uses member?. Both read the Bag items.
defmodule Bag do
defstruct items: []
end
defimpl Enumerable, for: Bag do
def reduce(_, {:halt, acc}, _fun), do: {:halted, acc}
def reduce(%Bag{items: []}, {:cont, acc}, _fun), do: {:done, acc}
def reduce(%Bag{items: [head | tail]}, {:cont, acc}, fun) do
reduce(%Bag{items: tail}, fun.(head, acc), fun)
end
def reduce(%Bag{} = bag, {:suspend, acc}, fun), do: {:suspended, acc, &reduce(bag, &1, fun)}
def count(%Bag{items: items}), do: {:ok, length(items)}
def member?(%Bag{items: items}, value), do: {:ok, value in items}
def slice(%Bag{}), do: {:error, __MODULE__}
end
bag = struct(Bag, items: [10, 20, 30])
IO.puts(Enum.count(bag))
IO.puts(Enum.member?(bag, 20))
IO.puts(Enum.member?(bag, 99))Enum transforms work through reduce
Enum.map and Enum.filter walk the Bag through reduce. They return new lists from its items.
defmodule Bag do
defstruct items: []
end
defimpl Enumerable, for: Bag do
def reduce(_, {:halt, acc}, _fun), do: {:halted, acc}
def reduce(%Bag{items: []}, {:cont, acc}, _fun), do: {:done, acc}
def reduce(%Bag{items: [head | tail]}, {:cont, acc}, fun) do
reduce(%Bag{items: tail}, fun.(head, acc), fun)
end
def reduce(%Bag{} = bag, {:suspend, acc}, fun), do: {:suspended, acc, &reduce(bag, &1, fun)}
def count(%Bag{items: items}), do: {:ok, length(items)}
def member?(%Bag{items: items}, value), do: {:ok, value in items}
def slice(%Bag{}), do: {:error, __MODULE__}
end
bag = struct(Bag, items: [10, 20, 30])
IO.inspect(Enum.map(bag, fn x -> x * 2 end))
IO.inspect(Enum.filter(bag, fn x -> x > 15 end))Example
defmodule Bag do
defstruct items: []
end
defimpl Enumerable, for: Bag do
def reduce(_, {:halt, acc}, _fun), do: {:halted, acc}
def reduce(%Bag{items: []}, {:cont, acc}, _fun), do: {:done, acc}
def reduce(%Bag{items: [head | tail]}, {:cont, acc}, fun) do
reduce(%Bag{items: tail}, fun.(head, acc), fun)
end
def reduce(%Bag{} = bag, {:suspend, acc}, fun), do: {:suspended, acc, &reduce(bag, &1, fun)}
def count(%Bag{items: items}), do: {:ok, length(items)}
def member?(%Bag{items: items}, value), do: {:ok, value in items}
def slice(%Bag{}), do: {:error, __MODULE__}
end
bag = struct(Bag, items: [5, 10, 15])
IO.inspect(Enum.to_list(bag))
IO.puts(Enum.count(bag))
IO.puts(Enum.member?(bag, 10))
IO.inspect(Enum.map(bag, fn x -> x * 2 end))Key points
- Reduce drives Enum: Enum functions walk a value through reduce, so reduce handles continue, halt, and suspend.
- Count answer: count returns a tuple with ok and the number of items.
- Member answer: member? returns a tuple with ok and true or false for a value.
- Slice fallback: slice can answer with an error tuple so Enum falls back to reduce.
- Custom struct: A struct with an Enumerable implementation can be passed to Enum like a list.
In this example, the Bag struct holds items in a list. Then an Enumerable implementation provides reduce, count, member?, and a slice that falls back to reduce. Finally Enum.to_list, Enum.count, Enum.member?, and Enum.map all work on the Bag, just as they work on a list.
Note
Protocols are consolidated at compile time, so dispatch stays fast.
Try it Yourself
Edit the code, then press Run.
Running is simulated
Exercise
Which function must an Enumerable implementation provide so Enum functions can walk its items?