I have a general question about default parameters and nil values. Suppose I have two functions. One calls the other (which is a helper function). Both have an optional parameter.
The helper function just joins a list to a string with a joiner. The joiner is passed to the first function inside an opts keyword list. The passing of the joiner is optional, it defaults to "AND"
defmodule ParamTest do
def func_1(list, opts \\ []) do
helper(list, opts[:joiner])
# Do something else with the result
end
defp helper(list, joiner \\ "AND") do
Enum.join(list, " #{joiner} ")
end
end
# Example 1
["el 1", "el 2"]
|> ParamTest.func_1(joiner: "AND")
# Result "el 1 AND el 2"
# Example 2
["el 1", "el 2"]
|> ParamTest.func_1
# Result: "el 1 el 2"
# But it should be also "el 1 AND el 2"
The problem is: opts[:joiner] will be nil in the second example. But it is still present so the default value will not be used.
One possible solution would be to use case
:
defmodule ParamTest do
def func_1(list, opts \\ []) do
case is_nil(opts[:joiner]) do
true -> helper(list)
false -> helper(list, opts[:joiner])
end
# Do something else with the result
end
defp helper(list, joiner \\ "AND") do
Enum.join(list, " #{joiner} ")
end
end
Another way would be to use two function definitions for helper and use pattern matching:
defmodule ParamTest do
def func_1(list, opts \\ []) do
case is_nil(opts[:joiner]) do
true -> helper(list)
false -> helper(list, opts[:joiner])
end
end
defp helper(list, nil) do
Enum.join(list, " AND ")
end
defp helper(list, joiner \\ "AND") do
Enum.join(list, " #{joiner} ")
end
end
But I have the feeling that this is not very elegant and could get messy in more complex situations.
What is a better solution for this scenario?
The best solution would be to make joiner mandatory in helper and provide default options in func_1
.
def func_1(list, opts \\ [joiner: "AND"]) do
helper(list, opts[:joiner])
...
end
defp helper(list, joiner) do
...
end
Always try to separate your concerns. helper
is not part of your public API, so you can always pass all the options. Let it just do its work and not worry about defaults.
func_1
is your public API and it should worry about defaults. You want to specify "AND" joiner by default, so do it instead of defaulting to empty option list. When developers read your code, they won't need to go deeper to check where does the "AND" come from and can easily figure out, that they can pass this option without reading docs or even function body.
It is usually a good idea to have defaults only for convenience at top level functions (the API) and just pass explicitly everything down. Otherwise, you would have to check at each level, if the option was passed like you did in your example with case
. This is error prone.
No better solution that what you already have in my opinion. Personally, I would ask myself the following questions:
helper/2
private function? I'm not confident about this, but I feel like default \\
args to private function may be some kind of code smell.\\
default argument? :)If I had to choose, in this particular case I'd probably go with calling helper/1
and helper/2
separately, based on the presence of the :joiner
option:
defmodule ParamTest do
def func_1(list, opts \\ []) do
if joiner = opts[:joiner] do
helper(list, joiner)
else
helper(list)
end
end
defp helper(list, joiner \\ "AND") do
Enum.join(list, " #{joiner} ")
end
end
However, as I stated above, since helper/2
is a private function it may make sense (depending on your use case, this one is too small for us to make a more thoughtful decision :P) to move the optional joiner completely to the boundary of the "system", i.e., just to func_1/2
by using a default value for the option:
defmodule ParamTest do
def func_1(list, opts \\ []) do
helper(list, opts[:joiner] || "AND")
end
defp helper(list, joiner) do
Enum.join(list, " #{joiner} ")
end
end
Again, this may not scale well in your use case but I feel like it's the best we can do with the information we have from the question :).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With