Definitions with multiple clauses and default values require a header

warning: definitions with multiple clauses and default values require a header. Instead of:

    def foo(:first_clause, b \\ :default) do ... end
    def foo(:second_clause, b) do ... end

one should write:

    def foo(a, b \\ :default)
    def foo(:first_clause, b) do ... end
    def foo(:second_clause, b) do ... end

def keep_lower_alphanumeric/2 has multiple clauses and defines defaults in one or more clauses
  lib/util/sanitizer.ex:16

My code:

  @doc """
  Lowercases a string and removes all non-alphanumeric characters.
  """
  @spec keep_lower_alphanumeric(binary) :: binary
  def keep_lower_alphanumeric(value, min_size \\ 2) when is_binary(value) do
    value
    |> String.downcase()
    |> String.replace(~r/[^a-z0-9]/, "")
    |> String.pad_trailing(min_size, "0")
  end
  def keep_lower_alphanumeric(_value, _min_size), do: {:error, "value must be a string"}

I don’t understand what the warning is trying to get me to do. Any pointers?

1 Like

Maybe just add this after @spec

def keep_lower_alphanumeric(value, min_size \\ 2)

Like this?!

  @doc """
  Lowercases a string and removes all non-alphanumeric characters.
  """
  @spec keep_lower_alphanumeric(binary) :: binary
  def keep_lower_alphanumeric(value, min_size \\ 2)
  def keep_lower_alphanumeric(value, min_size \\ 2) when is_binary(value) do
    value
    |> String.downcase()
    |> String.replace(~r/[^a-z0-9]/, "")
    |> String.pad_trailing(min_size, "0")
  end
  def keep_lower_alphanumeric(_value, _min_size), do: {:error, "value must be a string"}

The warming message is exactly right, but the first time it comes up its often a bit confusing. The reason for the warning is this:

You have two clauses for the same function:

 def keep_lower_alphanumeric(value, min_size \\ 2) when is_binary(value) do
 def keep_lower_alphanumeric(_value, _min_size), do:

One of the, the first, has a default value. The second does not. So its a but ambiguous to know what the default value is for the second clause. Is _min_size also a default of 2?

So Exlir requires you to but a function head with no body first. This function head has the default arguments on it so that the ambiguity goes away. And then the actual functions with function bodies don’t specify default arguments any more.

There is another benefit to this approach too. The argument names you use in the function head without a body, being the first of the function heads, will be used to generate the argument names for the @doc. This is helpful when you’re actually destructuring your arguments for pattern matching. For example:

def keep_lower_alphanumeric(binary, min_size \\ 2)
def keep_lower_alphanumeric(binary, %Decimal{amount: min_size}) do
  ...
end
def keep_lower_alphanumeric(binary, _min_size) do
  ...
end

It may be difficult for Elixir to work out what to name the second argument in this (artificial) example. Having the bare function head with simple names makes it more straight forward to describe the names of the arguments.

2 Likes

What would the end result of my code look like then @kip? I’m still a little confused, especially with the %Decimal{} parameter value.

Here’s what my code looks like now:

defmodule MyApp.Util.Sanitizer do
  @moduledoc """
  Sanitizer functions for Stitch data.
  """

  @doc """
  Lowercases a string and removes all non-alphanumeric characters.
  """
  @spec keep_lower_alphanumeric(binary) :: binary
  def keep_lower_alphanumeric(value, min_size \\ 2) when is_binary(value) do
    value
    |> String.downcase()
    |> String.replace(~r/[^a-z0-9]/, "")
    |> String.pad_trailing(min_size, "0")
  end
  def keep_lower_alphanumeric(_value, _min_size), do: {:error, "value must be a string"}
end

What would the end result look like? Appreciate the insight!

The %Decimal{} argument was just an example - maybe not such a good one. Using your code, all you need to do is the following:

defmodule MyApp.Util.Sanitizer do
  @moduledoc """
  Sanitizer functions for Stitch data.
  """

  @doc """
  Lowercases a string and removes all non-alphanumeric characters.
  """
  @spec keep_lower_alphanumeric(binary) :: binary
  def keep_lower_alphanumeric(value, min_size \\ 2)
  def keep_lower_alphanumeric(value, min_size) when is_binary(value) do
    value
    |> String.downcase()
    |> String.replace(~r/[^a-z0-9]/, "")
    |> String.pad_trailing(min_size, "0")
  end
  def keep_lower_alphanumeric(_value, _min_size), do: {:error, "value must be a string"}
end

Functionally, the pedant in me would say that this code also won’t support the large number of lower case letters that are not US ASCII but thats not the point of your post I know :slight_smile:

Thanks @kip - I had no idea we could declare function heads with no body. Does that default value of 2 ripple down to all function bodies unless I overwrite the default value?

def keep_lower_alphanumeric(value, min_size \\ 2)
def keep_lower_alphanumeric(value, min_size \\ 1989) when is_binary(value) and value == "Sergio" do
  value
  |> String.downcase()
  |> String.replace(~r/[^a-z0-9]/, "")
  |> String.pad_trailing(min_size, "0")
end

You can’t have different default arguments in different function heads - the guard clauses don’t serve to differentiate. They’re still all keep_lower_alphanumeric/2 function definitions.

That means you if you have multiple function clauses then defaults must be defined in a separate function head that has no body. Thats what the original warning was saying.

Gotcha - thanks again for the help appreciate it.