🎓 A1Learner
🔍
0 of 46 lessons completed

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?