A repository of bitesize articles, tips & tricks
(in both English and French) curated by Mirego’s team.

Elixir and default argument evaluation

Until very recently, I always assumed that these two Elixir modules would behave exactly the same:

defmodule Example1 do
  @default_now DateTime.utc_now()

  def foo(now \\ @default_now) do
    IO.puts(now)
  end
end

defmodule Example2 do
  def foo(now \\ DateTime.utc_now()) do
    IO.puts(now)
  end
end

Defining DateTime.utc_now() as the default argument value directly in the function declaration must mean that the value is evaluated at compile time (like it is when using module attributes), right? Nope.

Running this code:

Example1.foo()
Example2.foo()
Process.sleep(10000)
Example1.foo()
Example2.foo()

yields this:

2021-07-14 10:01:21.455025Z
2021-07-14 10:01:21.481010Z
2021-07-14 10:01:21.455025Z
2021-07-14 10:01:31.481404Z

So the value provided in the function declaration ends up being evaluated at runtime?! 🤯

According to the documentation:

The compiler translates this into multiple functions with different arities, […], that represent cases when arguments for parameters with default values are passed or not passed.

My understanding is that a function like this one:

def foo(now \\ DateTime.default_now()) do
  IO.puts(now)
end

ends up being compiled to:

def foo() do
  foo(DateTime.utc_now()) # ← See? Runtime!
end

def foo(now) do
  IO.puts(now)
end

Awesome! 👌