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