Craftsman at Work

I'm Artur Karbone, coding software architect and independent IT consultant and this is my blog about craftsmanship, architecture, distributed systems, management and much more.

Elixir Enum Module Under the Hood #2

In this series of posts we are gooing to look under the hood of Enum module. Here are the source and tests of the module.

Today our method under research is at. Let's go ahead, open iex and type h Enum.at .

iex> h Enum.at  

Help system shows that the method has two signatures (with and without default value if an element is not found):

 iex> Enum.at([2, 4, 6], 0)
 2 
 iex> Enum.at([2, 4, 6], 2)
 6 
 iex> Enum.at([2, 4, 6], 4)
 nil 
 iex> Enum.at([2, 4, 6], 4, :none)
 :none 

Indeed method has one signature with default parameter:

@doc """
  Finds the element at the given index (zero-based).
  Returns `default` if index is out of bounds.
  Note this operation takes linear time. In order to access
  the element at index `n`, it will need to traverse `n`
  previous elements.
  ## Examples
      iex> Enum.at([2, 4, 6], 0)
      2
      iex> Enum.at([2, 4, 6], 2)
      6
      iex> Enum.at([2, 4, 6], 4)
      nil
      iex> Enum.at([2, 4, 6], 4, :none)
      :none
  """
  @spec at(t, integer, default) :: element | default
  def at(collection, n, default \\ nil) do
    case fetch(collection, n) do
      {:ok, h} -> h
      :error     -> default
    end
  end

Under the hood the fetch method is being called:

def fetch(collection, n) when is_list(collection) and is_integer(n) and n >= 0 do  
    do_fetch(collection, n)
  end

As we see the method has a guard when is_list, which means that it is not possible to pass something other than list. Otherwise pattern matching is not going to be happy. Let's dive into the do_fetch method:

 defp do_fetch([h|_], 0), do: {:ok, h}
 defp do_fetch([_|t], n), do: do_fetch(t, n - 1)
 defp do_fetch([], _),    do: :error

The method basically decrements element's index recursively. When index is equal to zero head is being interpreted as an element we were looking for. If a parameter is an empty list and index is not equal to zero :error is being returned (this means that index is out of range).

Before reading Enum.at code I did my own naive implementation and it looks pretty similar to do_fetch method:

defmodule MyEnum do  
    def at([h|t],0), do: h  
    def at([h|t],n), do: at(t,n-1)  
    def at([],_),do: nil
end  

Looking at do_fetch i have to admit that substituting patterns, which are not being used with _ is a good idea. Slightly refactoring version of my own implementation would be:

defmodule MyEnum do  
    def at([h|_],0), do: h  
    def at([_|t],n), do: at(t,n-1)  
    def at([],_),do: nil
end  

That's it folks!

comments powered by Disqus