I have a library that injects functions in Ecto schemas through metaprogramming (GitHub - joeljuca/swiss_schema: A Swiss Army knife for your Ecto schemas). The functions are equivalent to Ecto’s Query and Schema APIs, so you can have a query toolkit without having to generate, and maintain, ctx modules just for basic database operations.
However, one of these functions is throwing a CompileError which I don’t fully understand. The following functions aggregate/2 and aggregate/3:
# The function bodies don't matter much to this issue
@impl SwissSchema
def aggregate(:count, opts \\ []) do
# ...
end
@impl SwissSchema
def aggregate(type, field, opts \\ []) do
# ...
end
Raises the following CompileError:
(CompileError) def aggregate/3 defaults conflicts with aggregate/2
This CompileError doesn’t make sense to me. How would these defaults conflict with each other? I’ve tried to add guard clauses to it, but I still had no success.
Elixir’s default arguments are not technically defaults. \\ is part of the def macro that writes a new function with the appropriate arity for the given default.
defmodule Foo do
def bar(baz \\ "baz") do
baz
end
end
gets rewritten to:
defmodule Foo do
def bar do
"baz"
end
def bar(baz) do
baz
end
end
How would you distinguish calling aggregate/2 without relying on the default parameter vs wanting aggregate/3 hoping for the default?
There are answers to that question, but most of those answers depend on what I know as a human looking at the code which is more than the compiler can assume. That’s the compiler’s complaint: it can’t figure out the difference between aggregate/2 when both parameters are given and aggregate/3 when you’re relying on the default: both are looking for two parameters.
@impl SwissSchema
def aggregate(:count) do
aggregate (:count, [])
end
@impl SwissSchema
def aggregate(type, field_or_opts) when is_keyword(field_or_opts) do
aggregate(type, field_or_opts, [])
end
def aggregate(type, field_or_opts) do
# ...
end
@impl SwissSchema
def aggregate(type, field, opts) do
# ...
end
@impl SwissSchema
# 1
def aggregate(type, opts \\ [])
# 2
def aggregate(:count, opts) when is_list(opts) do
unquote(repo).aggregate(__MODULE__, :count, opts)
end
# 3
def aggregate(type, field) when is_atom(field) do
aggregate(type, field, [])
end
@impl SwissSchema
# 4
def aggregate(type, field, opts) do
unquote(repo).aggregate(__MODULE__, type, field, opts)
end
I’ve placed numbered comments in each part of the solution, so I can comment them separately.
aggregate(type, opts \\ []) sets up aggregate/1 and aggregate/2. This is needed to allow aggregate(:count) calls – a special case in Ecto.Repo’s aggregate/3 callback (PS: I don’t like it much, but I’m supporting it for compatibility and DX)
aggregate(:count, opts) when is_list(opts) accounts for aggregate(:count, [ ... ]) calls
aggregate(type, field) when is_atom(field) is what seems to be solving the issue. This is a special case for SwissSchema’s aggregate/2 callback: if it’s used with an atom as the second argument, it means the caller is actually trying to use aggregate/3 with its default empty-list opts argument, so I call it directly
aggregate(type, field, opts) is SwissSchema’s aggregate/3 callback (no changes)
I would also be happy to hear what you think of this solution.