When I run the following Elixir script, it prints out {"K", "Y", "L"}.

defmodule MyMod do def make_tuple(a \\ "X", b \\ "Y", c) do {a, b, c} end end IO.inspect(MyMod.make_tuple("K", "L")) 

Why doesn't it output {"X", "K", "L"}? What is the mechanism behind this behavior?

3 Answers

You are correct: Elixir uses any "excess" arguments provided to override a function's default values for some or all of its parameters, filled from left to right. My understanding of this came from the Programming Elixir 1.6 book by Dave Thomas.

The trick is that for functions with default arguments, the compiler actually generates multiple functions. A function defined like this:

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

Actually gets compiled down to a module that looks something like this:

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 

By the way, you can see this by using a module's "magic" module_info/0 function, e.g. Example.module_info().

Calling your functions with some sample values will help you understand how they are interpreted, and there may be a few surprises.

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"] 

It may be a bit easier to see this behavior with a function like this:

def func(p1 \\ 1, p2 \\ 2) do IO.inspect [p1, p2] end 

And calling it with only 1 argument provided. It will use the provided argument on the left side; the default on the right still gets used:

func("a") # => ["a", 2] 

I agree that things do get confusing when a function has optional arguments that aren't at the end of the argument list.

Just from experimenting with it a bit, it seems like it assigns the arguments from the left. So in this case:

defmodule MyMod do def make_tuple(a \\ "X", b, c \\ "Y", d) do {a, b, c, d} end end IO.inspect(MyMod.make_tuple(1, 2, 3)) 

You get {1, 2, "Y", 3} - this is a "left-most" interpretation of the arguments provided. This leads to a situation where if you add an argument "at the end" it changes the meaning of the previous arguments, and (I assume) is the reason why people usually avoid having optional arguments that are not at the end of the argument list.

Long story short, Pawel is correct.

To understand why this happens, you should most probably look at what happens when a def macro is used. This is not as easy as it seems, since this is one of the bootsrap functions required to compile elixir, so it is written in erlang.

What we are actually interested in is the function unpack_defaults, that is one of the functions that handles default arguments, here we can notice the lines:

unpack_defaults(Kind, Meta, Name, [{'\\\\', DefaultMeta, [Expr, _]} | T] = List, Acc, Clauses) -> body... unpack_defaults(Kind, Meta, Name, T, [Expr | Acc], [Clause | Clauses]); unpack_defaults(Kind, Meta, Name, [H | T], Acc, Clauses) -> unpack_defaults(Kind, Meta, Name, T, [H | Acc], Clauses); unpack_defaults(_Kind, _Meta, _Name, [], Acc, Clauses) -> {lists:reverse(Acc), lists:reverse(Clauses)}. 

We don't care about details of how default arguments get sorted, however you can notice that the 4-th argument of every function is the actual list of arguments, then we have Acc witch are the resulting arguments and Clauses that are the defaults.

Both of them are added to the list using [Clause | Clauses] and [H | Acc] resulting in a reverse order list, and are reversed as the last return {lists:reverse(Acc), lists:reverse(Clauses)}.

Your Answer

Sign up or log in

Sign up using Google Sign up using Facebook Sign up using Email and Password

Post as a guest

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.