rigotti.nl

Default function arguments in Elixir

Published on Nov 12, 2024

Learning a new programming language can be an enlightening process. In general, the best return on investment for broadening one's knowledge is focusing on what makes a language stand out. However, some small features, those outside the spotlight, may also teach you a thing or two and challenge the mental models one may think are solid.

I've started learning Elixir because of OTP and metaprogramming, but this is about how default values for function arguments work.

Default values for function arguments aren't a big deal since many languages implement them. Take, for instance, these JS and PHP snippets where my custom-defined logarithm function takes two arguments, a number x which is applied to the logarithm base y. When y is not defined, it defaults to the mathematical constant e (2.718281828459045...). Hence becoming a natural logarithm function.

function myLog(x, y = Math.E) {
	return Math.log(x) / Math.log(y);
}

myLog(10) // 2.302585092994046
myLog(10) == Math.log(10) // true
myLog(8, 2) // 3
myLog(8, 2) == Math.log2(8) // true

and in PHP...

function myLog($x, $y = M_E) {
	return log($x) / log($y);
}

[!NOTE] For the sake of illustration I am writing it in PHP, however, the language's standard log function already accepts a second argument as the base which defaults as well to e 🤷‍♂️

In Elixir we would have yet another similar-looking function, except that we need to wrap a module around and define the e value ourselves.

defmodule MyMath do
	@e :math.exp(1) # 2.718281828459045

  def my_log(x, y \\ @e) do
    :math.log(x) / :math.log(y)
  end
end

MyMath.my_log(10) # 2.302585092994046
MyMath.my_log(8, 2) # 3.0

Okay, but when we see logarithm functions outside programming, the base often comes before the number it's applied to. Something that looks like this:

$$ \log_y x $$ What if I didn't know better and naively changed my JS code to look like this?

function myLog(y = Math.E, x) {
	return Math.log(x) / Math.log(y);
}

myLog(2, 8) // 3
myLog(10) // NaN

No, I can't. As you can see, even though the JS compiler allows me to define a function with its first argument with a default value. It won't work when I call it with a single argument, rendering this definition incorrect and error-prone. How would PHP fare with this change?

function myLog($y = M_E, $y) {
	return log($x) / log($y);
}

# Deprecated: Optional parameter $y declared before required parameter $x is implicitly treated as a required parameter

More restrictive than JS's compiler, PHP won't let me go further by stating that I can't define a default parameter before a required one. That's an easy mental model to accept, right?

What about Elixir?

defmodule MyMath do
	@e :math.exp(1) # 2.718281828459045

  def my_log(y \\ @e, x) do
    :math.log(x) / :math.log(y)
  end
end

MyMath.my_log(10) # 2.302585092994046
MyMath.my_log(2, 8) # 3.0

🤯

Wait, what? How does Elixir allow me to do it? How is it computing the correct value when only a single argument is passed?

If you already know the answer, congratulations, you won't learn anything new with this article. In case you don't... can you guess it before reading the solution in the next paragraph?

It took me some experimentation, but the answer is rather simple. Whenever I call my_log with a single argument, Elixir will replace the first required argument, namely x, not the first positioned argument, which now will be y. However, when I call it with two arguments, it will replace them, following them according to their position.

def my_log(y \\ @e, x) do ... end
my_log(10) == my_log(@e, 10)
my_log(2, 8) == my_log(2, 8) # business as usual

# With three arguments I can even have a default value in the middle
def my_fun(x, y \\ 10, z) do ... end
my_fun(1, 3) == my_fun(1, 10, 3)
my_func(1, 2, 3) == my_fun(1, 2, 3)

[!NOTE] Interestingly enough, when I prompted a highly regarded LLM with the question: "how do default function arguments work in Elixir?" part of the answer was delusional when it stated that:

Default arguments must be placed at the end of the parameter list. You cannot have a non-default argument after a default argument.

Is this groundbreaking? I don't think so, but I don't know any other languages that do it too. Are JavaScript and PHP wrong by not allowing default arguments before "required" ones? Also no, one might argue that this is a matter of taste or that it can lead to confusion, I could even have rewritten my function to look like this:

function myLog(y, x) {
	if (typeof x === 'undefined') return myLog(Math.E, y)
	return Math.log(x) / Math.log(y)
}

myLog(2, 8) // 3
myLog(10) // 2.302585092994046

What I found the most interesting about this discovery is that I never challenged the concept that required parameters shouldn't come before optional ones in a function signature. The relearning process can be quite liberating if you are open to it. So, by all means, learn a new language to land a new job or understand some complex concept, just remember to pay attention to the small details, it will make the ride.