Pattern matching on function heads

These are two distinct functions in my code. The first one takes in a map and optionally, a list. The second one takes in a list. I don’t understand where the conflict is.

def list_recurring_orders(%{subaccount_id: subaccount_id}, opts \\ []) when is_integer(subaccount_id)

def list_recurring_orders(opts) when is_list(opts)

I can’t use them because I get this error:

** (CompileError) lib/app_core/orders/recurring_orders.ex:155: def list_recurring_orders/1 conflicts with defaults from list_recurring_orders/2

The issue arises because despite appearances, using default arguments actually creates two function heads - one with arity 1 and one with arity 2. So your code expands to:

def list_recurring_orders(%{subaccount_id: subaccount_id}, opts) do
  ....
end

def list_recurring_orders(%{subaccount_id: subaccount_id} = account) when is_integer(subaccount_id) do
  list_recurring_orders(account, [])
end
def list_recurring_orders(opts) when is_list(opts) do
 ...
end

To resolve this the defaults need to go in a bodiless function head:

def list_recurring_orders(account, opts \\ []) 
def list_recurring_orders(%{subaccount_id: subaccount_id}, opts) when is_integer(subaccount_id)

def list_recurring_orders(opts) when is_list(opts)
4 Likes

I get this error when I put the bodiless function heads before the functions.

 only variables and \\ are allowed as arguments in function head.

If you did not intend to define a function head, make sure your function definition has the proper syntax by wrapping the arguments in parentheses and using the do instruction accordingly:

    def add(a, b), do: a + b

    def add(a, b) do
      a + b
    end

I’d need to see what your full definition was, but this certainly compiles fine:

defmodule T do
  def add(a, b \\ 1)

  def add(a, b), do: a + b

  def add(a, b) do
    a + b
  end
end
2 Likes
def list_recurring_orders(%{subaccount_id: subaccount_id}, opts \\ []) when is_integer(subaccount_id)
def list_recurring_orders(opts) when is_list(opts)

as kip said is just another way of writing

def list_recurring_orders(args), do: def list_recurring_orders(args, [])
def list_recurring_orders(%{subaccount_id: subaccount_id}, opts) when is_integer(subaccount_id)
def list_recurring_orders(opts) when is_list(opts)

and now the last definition is unreachable - or in other words conflicting with the defaults.

def list_recurring_orders(%{subaccount_id: subaccount_id}, opts) when is_integer(subaccount_id) do
...
end
def list_recurring_orders(opts) when is_list(opts) do
...
end
def list_recurring_orders(%{subaccount_id: subaccount_id}) do
...
end

that would probably work, but it is a little awkward if you ask me.

1 Like

I’m still having issues based on kip’s solution. I added this function head: def list_recurring_orders(subaccount_id, opts \\ []) and it’s not doing the trick.

Are you saying write three functions?

Yes, seems that’s a requirement in this case to remove ambiguity:

  def list_recurring_orders(%{subaccount_id: subaccount_id}, opts) when is_integer(subaccount_id) do
     # code here
  end
  
  def list_recurring_orders(%{subaccount_id: _} = sub_account) do
    list_recurring_orders(sub_account, [])
  end
  
  def list_recurring_orders(opts) when is_list(opts) do
    # code here
  end
1 Like

Maybe.

It really depends on what you are trying to achieve. Right now the way I understand your example, I would probably do something like this

def list_recurring_orders(args, opts \\ [])

def list_recurring_orders(%{subaccount_id: subaccount_id}, opts) when is_integer(subaccount_id) do
# list all recurring orders for subaccount
end

def list_recurring_orders(:all, opts) do
# list all recurring orders for all accounts (?)
end


iex> list_recurring_orders(:all)
[%Order{}]

iex> list_recurring_orders(%{subaccount_id: 1})
[%Order{}]

iex> list_recurring_orders(%{subaccount_id: 1}, with_preloads: [:items])
[%Order{items: [%Item{}]}]

I think the problem here is that you want to define 2 default values, which is possible but is not the best option for code readabilty imo.

This is exactly what I want to do; I’ll try passing in :all as a flag.