About ambiguity introduced in function default arguments

Hi, I am learning Elixir and recently reading the Supervisor source code, and came accross start_link/2 and start_link/3:

# https://github.com/elixir-lang/elixir/blob/v1.20.1/lib/elixir/lib/supervisor.ex#L733
def start_link(children, options) when is_list(children) do

# https://github.com/elixir-lang/elixir/blob/v1.20.1/lib/elixir/lib/supervisor.ex#L976
def start_link(module, init_arg, options \\ []) when is_list(options)

As far as I understand, the default argument in def start_link(module, init_arg, options \\ []) should cause another start_link/2 to be defined and should result in ambiguity ? Does guard helps the compiler reduce ambiguity and thus the compiler treats it as a second clause for start_link/2 ? If so, the two clauses does not need to be grouped together ?

I tried the following:

defmodule A do
  def f(a), do: "f(#{inspect(a)})"

  def f(a, b \\ []) when is_list(b), do: "f(#{inspect(a)}, #{inspect(b)})"
end

and result in a warning:

➜  /tmp iex test.ex
Erlang/OTP 27 [erts-15.2] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [jit:ns]

    warning: this clause for f/1 cannot match because a previous clause at line 2 always matches
    β”‚
  4 β”‚   def f(a, b \\ []) when is_list(b), do: "f(#{inspect(a)}, #{inspect(b)})"
    β”‚       ~
    β”‚
    └─ test.ex:4:7

Interactive Elixir (1.19.0-dev) - press Ctrl+C to exit (type h() ENTER for help)

But if I changed the order of the def’s:

defmodule A do
  def f(a, b \\ []) when is_list(b), do: "f(#{inspect(a)}, #{inspect(b)})"

  def f(a), do: "f(#{inspect(a)})"
end

it results in an error:

➜  /tmp iex test.ex
Erlang/OTP 27 [erts-15.2] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [jit:ns]

    error: def f/1 conflicts with defaults from f/2
    β”‚
  4 β”‚   def f(a), do: "f(#{inspect(a)})"
    β”‚       ^
    β”‚
    └─ test.ex:4:7: A.f/1

** (CompileError) test.ex: cannot compile module A (errors have been logged)
    test.ex:4: (module)

Can anybody point me to related documentation about this behavior difference ?

The difference is that in Supervisor example you have provided there is guard, which limits about of cases it will match.

So the expanded code will look like

def a(x) when is_list(x), do: …

def a(x), do: a(x, :default)

def a(x, y), do: …

So as you can see, there is a situation when 1st clause match.

If you swap order of these functions, then it will mean that there would be no option to call first clause.

I still don’t understand why switching the order would switch between warning and error.

Sidenote: Please upgrade the toolchain to Elixir1.20.1 / OTP29.

The compiler explicitly handles \\ operator because Erlang does not have default argument values. It does not allow any redeclaration after \\ has been expanded. Look-behind, though, would have a ton of redundant tracking involved, decreasing performance, that’s why it does not blow up immediately.

The error you observe is raised by first-pass compiler, meeting the \\ default parameters within the function redeclaration.

The warning you observe if you swap clauses is the second-pass compiler, simply warning you about unreachable clause, it has nothing to do with \\ because there is no look-behind.