Craftsman at work

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

Elixir Enum Module Under the Hood #1

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.

Let's start with the count method. Let's go ahead and open iex and type h Enum.count .

iex> h Enum.count  

Help system shows that the method has two signatures (with and without lambda) :

 iex> Enum.count([1, 2, 3])
 3
 iex> Enum.count([1, 2, 3, 4, 5], fn(x) -> rem(x, 2) == 0 end)
 2

Here is the first one:

@doc """
  Returns the collection's size.
  ## Examples
      iex> Enum.count([1, 2, 3])
      3
  """
  @spec count(t) :: non_neg_integer
  def count(collection) when is_list(collection) do
    :erlang.length(collection)
  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 satisfied.
In addtion to that we see that internally the count method uses erlang.length method. Btw. looking at the code we have a glue how to document it via @doc directive.

There is another pattern matching for the same signature:

def count(collection) do  
    case Enumerable.count(collection) do
      {:ok, value} when is_integer(value) ->
        value
      {:error, module} ->
        module.reduce(collection, {:cont, 0}, fn
          _, acc -> {:cont, acc + 1}
        end) |> elem(1)
    end
  end

This pattern matching basically works with collections. For instance:

iex> Enum.count 1..100  

Internally Enumerable.count method is being called, which returns a tuple {:ok,value} (in this case value is being extracted from pattern matching and returned to the caller) or {:error,module}.

Let's dive into the second signature, wich allows to pass lambda and filter out the results.

@doc """
  Returns the count of items in the collection for which
  `fun` returns a truthy value.
  ## Examples
      iex> Enum.count([1, 2, 3, 4, 5], fn(x) -> rem(x, 2) == 0 end)
      2
  """
  @spec count(t, (element -> as_boolean(term))) :: non_neg_integer
  def count(collection, fun) do
    Enumerable.reduce(collection, {:cont, 0}, fn(entry, acc) ->
      {:cont, if(fun.(entry), do: acc + 1, else: acc)}
    end) |> elem(1)
  end

Again internally this method leverages reduce method. We are going to research the way reduce works internally later on. For now You can think of reduce like it is an accumulator wich receives a collection, accumulator value, current element of collection (while iterating through) and of course lambda which receives current element and accumulator as parameters.

Now let's see how tests are implemented. Basically there are two testing scenarios for each pattern matching and for each method signature. The tests are written against empty and respectivelly non empty lists/collections of different size:

  test :count do
    assert Enum.count([1, 2, 3]) == 3
    assert Enum.count([]) == 0
  end

  test :count_fun do
    assert Enum.count([1, 2, 3], fn(x) -> rem(x, 2) == 0 end) == 1
    assert Enum.count([], fn(x) -> rem(x, 2) == 0 end) == 0
  end

   test :count do
    range = 1..5
    assert Enum.count(range) == 5
    range = 1..1
    assert Enum.count(range) == 1

    assert Enum.count([1, true, false, nil]) == 4
  end

  test :count_fun do
    range = 1..5
    assert Enum.count(range, fn(x) -> rem(x, 2) == 0 end) == 2
    range = 1..1
    assert Enum.count(range, fn(x) -> rem(x, 2) == 0 end) == 0

    assert Enum.count([1, true, false, nil], & &1) == 2
  end
comments powered by Disqus