Elixir’s new continuable enumerators¶
A recap of enumerators, and some terminology¶
ex. How you can count the total length of strings in a list¶
Enumerable.reduce(l, 0, fn x, acc -> String.length(x) + acc end)
Two issues with classic Elixir enumerators¶
Enumrators’ limitations¶
- It’s not possible to define a function that only returns at most 3 elements without traversing all elements or using ugly tricks such as “throw”
- There’s no way to interleave two enumerables.
The underlying problem¶
The producer is fully in control.
There’s no way aside from “throw”/”raise” for a consumer to tell a producer “stop producing”.
There’s definitely no way to tell a producer “stop for now but be prepared to continue where you left off later”.
Power to the consumer!¶
Instead of returning only an accumulator at every step (for every produced value) the consumer returns a combination of an accumulator and an instruction to the producer.
Three instructions are available:
- :cont
- Keep producing
- A consumer that always returns :cont makes the producer behave exactly the same as in the old system.
- :halt
- Stop producing
- A consumer may return :halt to have the producer terminate earlier than it normally would.
- :suspend
- Temporarily stop producing.
- tells a producer to return the accumulator and a continuation function.
{ :suspended, n_, cont } = Enumerable.reduce(1..5, { :cont, 0 }, fn x, n ->
if x == 3 do
{ :suspend, n }
else
{ :cont, n + x }
end
end)
Implementing “interleave”¶
defmodule Interleave do
def interleave(a, b) do
step = fn x, acc -> { :suspend, [x|acc] } end
af = &Enumerable.reduce(a, &1, step)
bf = &Enumerable.reduce(b, &1, step)
do_interleave(af, bf, []) |> :lists.reverse()
end
defp do_interleave(a, b, acc) do
case a.({ :cont, acc }) do
{ :suspended, acc, a } ->
case b.({ :cont, acc }) do
{ :suspended, acc, b } ->
do_interleave(a, b, acc)
{ :halted, acc } ->
acc
{ :done, acc } ->
finish_interleave(a, acc)
end
{ :halted, acc } ->
acc
{ :done, acc } ->
finish_interleave(b, acc)
end
end
defp finish_interleave(a_or_b, acc) do
case a_or_b.({ :cont, acc }) do
{ :suspended, acc, a_or_b } ->
finish_interleave(a_or_b, acc)
{ _, acc } ->
acc
end
end
end
Interleave.interleave([1,2], [:a, :b, :c, :d])
#=> [1, :a, 2, :b, :c, :d]