Improper docs for Enum.find?

I’m looking at the official docs for Enum.find and I am baffled…

  1. How can you have a default value for an argument that is not the last argument? What’s the behavior there?
  2. Related: why does Enum.find seem to work when you call it with only 2 arguments? (This probably will answer my first question)

Looking at the current source code (Elixir 1.10.3 I believe) does not help me make sense of this behvior:

  @doc """
  Returns the first element for which `fun` returns a truthy value.
  If no such element is found, returns `default`.

  ## Examples

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

      iex> Enum.find([2, 4, 6], fn x -> rem(x, 2) == 1 end)
      nil
      iex> Enum.find([2, 4, 6], 0, fn x -> rem(x, 2) == 1 end)
      0

  """
  @spec find(t, default, (element -> any)) :: element | default
  def find(enumerable, default \\ nil, fun)

  def find(enumerable, default, fun) when is_list(enumerable) do
    find_list(enumerable, default, fun)
  end

  def find(enumerable, default, fun) do
    Enumerable.reduce(enumerable, {:cont, default}, fn entry, default ->
      if fun.(entry), do: {:halt, entry}, else: {:cont, default}
    end)
    |> elem(1)
  end

How exactly is that function head being handled?

Thanks for clarifications!

If you want a default return value you pass 3 arguments, the second will be the default.

The function accepts 2 arguments, because in that case it sets the default return to nil (the first definition in the source you excerpted).

From the Programming Elixir 1.6 book by Dave Thomas

When you call a function that is defined with default parameters, Elixir compares the number of arguments you are passing with the number of required parameters for the function. If you’re passing fewer arguments than the number of required parameters,
then there’s no match. If the two numbers are equal, then the required parameters take the values of the passed arguments, and the other parameters take their default values. If the count of passed arguments is greater than the number of required parameters, Elixir uses the excess to override the default values of some or all parameters. Parameters are matched left to right.

defmodule Example do 
  def func(p1, p2 \\ 2, p3 \\ 3, p4) do 
    IO.inspect [p1, p2, p3, p4]
  end 
end

Example.func("a", "b")           # => ["a",2,3,"b"]
Example.func("a", "b", "c")      # => ["a","b",3,"c"]
Example.func("a", "b", "c", "d") # => ["a","b","c","d"]
4 Likes

The trick here is that for functions with default arguments the compiler actually generates multiple functions, one for specific number of arguments. So for you func example:

defmodule Example do 
  def func(p1, p2 \\ 2, p3 \\ 3, p4) do 
    IO.inspect [p1, p2, p3, p4]
  end 
end

You will actually get a module which looks something like:

defmodule Example do 
  def func(p1, p4) do
    func(p1, 2, 3, p4)
  end

  def func(p1, p2, p4) do
    func(p1, p2, 3, p4)
  end

  def func(p1, p2, p3, p4) do 
    IO.inspect [p1, p2, p3, p4]
  end 
end

The erlang/elixir system cannot handle functions with multiple number of arguments but it can handle having functions with the same name but a different number of arguments which are actually different functions. When you call a function with that name you get the function which has the same number of arguments as in your call. If there isn’t one you get an error. This is what is happening here.

To see the functions which have been generated try calling the module_info/0 function which the compiler automatically generates, in this case Example.module_info().

9 Likes