Range Over Iterators in Elixir
Starting with version 1.23, support has been added for iterators, which lets us range over pretty much anything!
Let’s look at the List
type from the previous example again. In that example we had an AllElements
method that returned a slice of all elements in the list. With iterators, we can do it better - as shown below.
defmodule List do
defstruct head: nil, tail: nil
defmodule Element do
defstruct next: nil, val: nil
end
def new(), do: %List{}
def push(%List{tail: nil} = lst, v) do
new_element = %Element{val: v}
%List{lst | head: new_element, tail: new_element}
end
def push(%List{tail: tail} = lst, v) do
new_element = %Element{val: v}
tail = %Element{tail | next: new_element}
%List{lst | tail: new_element}
end
def all(%List{head: head}) do
fn yield ->
iterate_elements(head, yield)
end
end
defp iterate_elements(nil, _yield), do: nil
defp iterate_elements(%Element{val: val, next: next} = _element, yield) do
if yield.(val) do
iterate_elements(next, yield)
else
nil
end
end
end
defmodule Main do
def run() do
lst = List.new()
|> List.push(10)
|> List.push(13)
|> List.push(23)
Enum.each(lst |> List.all(), fn val -> IO.puts(val) end)
all = Enum.to_list(lst |> List.all())
IO.puts("all: #{inspect(all)}")
Enum.each(gen_fib(), fn n ->
if n >= 10 do
break
end
IO.puts(n)
end)
end
def gen_fib() do
fn yield ->
iterate_fib(1, 1, yield)
end
end
defp iterate_fib(a, b, yield) do
if yield.(a) do
iterate_fib(b, a + b, yield)
else
nil
end
end
end
Main.run()
All returns an iterator, which is a function that takes another function called yield
by convention (but the name can be arbitrary). It will call yield
for every element we want to iterate over and note yield
’s return value for a potential early termination.
Iteration doesn’t require an underlying data structure, and doesn’t even have to be finite! Here’s a function returning an iterator over Fibonacci numbers: it keeps running as long as yield
keeps returning true
.
Since List.all
returns an iterator, we can use it in a regular loop.
lst = List.new()
|> List.push(10)
|> List.push(13)
|> List.push(23)
Enum.each(lst |> List.all(), fn val -> IO.puts(val) end)
Packages like Enum
have a number of useful functions to work with iterators. For example, Enum.to_list
takes any iterator and collects all its values into a list.
all = Enum.to_list(lst |> List.all())
IO.puts("all: #{inspect(all)}")
Once the loop hits break
or an early return, the yield
function passed to the iterator will return false
.
Enum.each(gen_fib(), fn n ->
if n >= 10 do
break
end
IO.puts(n)
end)