eeff
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 ?
Marked As Solved
mudasobwa
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.
Also Liked
mudasobwa
Here is the documentation:
If a function with default values has multiple clauses, it is required to create a function head (a function definition without a body) for declaring defaults — Modules and functions — Elixir v1.20.2
Just don’t try to hack the proposed syntax. Supervisor also ditched this cludge, that’s why I suggested to use the latest versions of the toolchain.
odelbos
You need to do it like this:
defmodule A do
def f(a, b \\ [])
def f(a, []), do: "f(#{inspect(a)})"
def f(a, b) when is_list(b), do: "f(#{inspect(a)}, #{inspect(b)})"
end
IO.puts A.f(5)
# Output --> f(5)
IO.puts A.f(5, [:one, :two])
# Output --> f(5, [:one, :two])
odelbos
It’s because of the declarative f/1 order.
When we write:
def f(a, b \\ [])
I fact the compiler will expand it in two functions:
def f(a), do: f(a, []) # <-- This is not a declarative f/1 it's the default-argument of f/2
def f(a, b) do: ...
So in the first case
defmodule A do
def f(a), do: "f(#{inspect(a)})" # <--- declarative f/1
def f(a, b \\ []) when is_list(b), do: ... # <--- will be expanded by the compiler
end
You will have two f/1 but with the declarative f/1 in first position (top-bottom) will match all clauses. So the compiler emit a warning for the unreachable expanded f/1 that handle the default-argument case of f/2.
In the second case:
defmodule A do
def f(a, b \\ []) when is_list(b), do ... # <--- will be expanded by the compiler
def f(a), do: "f(#{inspect(a)})" # <--- declarative f/1
end
The declarative f/1 is after the expanded f/1 but from the compiler’s perspective, the expanded f/1 is not a declarative f/1 function it’s the mechanism that handle the default-argument of the f/2 function. So when later the compiler encounter the declarative f/1 which is in conflict with the auto-generated f/1 it emit a compilation error.
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance









