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!