loading...
Cover image for Iterating Tuples in Elixir

Iterating Tuples in Elixir

mudasobwa profile image Aleksei Matiushkin Originally published at rocket-science.ru on ・2 min read

According to erlang docs,

A tuple is a compound data type with a fixed number of terms:

{Term1,...,TermN}

Each term Term in the tuple is called an element. The number of elements is said to be the size of the tuple.

Tuples are known to be faster than Lists (and even Maps) for elements’ direct access. Also, some libraries/packages often format a response as tuples.

So, Tuples are good. Save for one single glitch: they are not iterable. That said, Tuples do not implement Enumerable protocol. The goal of this small post is to show how this protocol might be implemented for tuples in a non-naïve way (the naïve way would be to just convert tuples to lists.)

defmodule Tuple.Enumerable do
  defimpl Enumerable, for: Tuple do
    @max_items 42

    def count(tuple), do: tuple_size(tuple)

    # member? implementation is done through casting tuple to the list
    # it’s not required for iteration, and building all those matched
    # clauses seems to be an overkill here
    def member?([], _), do: {:ok, false}
    def member?(tuple, value) when is_tuple(tuple) do
      tuple |> Tuple.to_list |> member?(value)
    end
    def member?(tuple, value) when is_list(tuple) do
      for [h | t] <- tuple do
        if h == value, do: {:ok, true}, else: member?(t, value)
      end
    end

    def reduce(tuple, acc, fun) do
      do_reduce(tuple, acc, fun)
    end

    defp do_reduce(_, {:halt, acc}, _fun), do: {:halted, acc}
    defp do_reduce(tuple, {:suspend, acc}, fun) do
      {:suspended, acc, &do_reduce(tuple, &1, fun)}
    end
    defp do_reduce({}, {:cont, acc}, _fun), do: {:done, acc}
    defp do_reduce({value}, {:cont, acc}, fun), do: do_reduce({}, fun.(value, acc), fun)

    Enum.each(1..@max_items-1, fn tot ->
      tail = Enum.join(Enum.map(1..tot, & "e_#{&1}"), ",")
      match = Enum.join(["value"] ++ [tail], ",")

      defp do_reduce({unquote(match)}, {:cont, acc}, fun) do
        do_reduce({unquote(tail)}, fun.("value", acc), fun)
      end
    end)

    # list fallback for huge tuples
    defp do_reduce([h | t], {:cont, acc}, fun) do
      do_reduce((if Enum.count(t) <= @max_items, do: List.to_tuple(t), else: t), fun.(h, acc), fun)
    end

    # fallback to list for huge tuples
    defp do_reduce(huge, {:cont, acc}, fun) when huge > @max_items do
      do_reduce(Tuple.to_list(huge), {:cont, acc}, fun)
    end
  end
end

The code above builds @max_items function match clauses for Tuples having not more than 42 members. That guarantees no conversion to listhappens for those.

Huge Tuples, having more than 42 members, will be converted to lists before operating.

The approach above might be useful in the case when one needs to useTuples for quick access: changing @max_items to super high value would drastically increase the compilation time, while the execution time will remain very impressive, comparing to Lists.

Posted on Dec 7 '17 by:

mudasobwa profile

Aleksei Matiushkin

@mudasobwa

NB! I am _not_ a member of #DEVCommunity. → I like: Elixir, Erlang, Ruby, R, C, COBOL. → I hate: Apple, JS, Rails, haters. → I am more functional, than object oriented.

Discussion

markdown guide