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)