Elixir: How to deal with optional / default parameters in functions and nil values?

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:

  • am I sure I need a default argument for the 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.
  • where do I want to handle the complexity if I go with a \\ 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 :).

Tags:

Elixir