Overloaded functions with the same name and arity?

I feel like this function is perfectly ergonomic:

# default value in effect: count \\ :all
@spec delete(t(e), e) :: t(e) when e: term()
@spec delete(t(e), e, :strict) :: t(e) when e: term()

# default value in effect: count \\ :all
@spec delete(t_lax(e), e) :: t_lax(e) when e: term()
@spec delete(t_lax(e), e, :lax) :: t_lax(e) when e: term()

@spec delete(t(e), e, :all | non_neg_integer()) :: t(e) when e: term()
@spec delete(t(e), e, :all | non_neg_integer(), :strict) :: t(e) when e: term()

@spec delete(t_lax(e), e, :all | non_neg_integer()) :: t_lax(e) when e: term()
@spec delete(t_lax(e), e, :all | non_neg_integer(), :lax) :: t_lax(e) when e: term()

So, is there any way to make the rendered exdoc actually reflect that?

I feel like the documentation tooling has produced a hideous, unreadable mess; it looks like it’s somehow crosswise of the overloads and defaults, having completely segregated all the different @spec directives by arity, rather than by semantic group… but I have no clue what I could to do improve it.

As far as I know no and I don’t think there would be ever support for that not only in the documentation, but also in Elixir itself. The compiler should warn you that functions with the same name and arity should be grouped together.

You can give different names to all related functions. for example by simply adding _lax suffix and then you can group specific functions. For more information take a look at:
Grouping functions, types, and callbacks | mix docs task @ ex_doc documentation

1 Like

I feel like this is multiple functions in a trench coat

def decrease_strict(ms, element, count)
def decrease_lax(ms, element, count)
def delete_strict(ms, element)
def delete_lax(max, element)
3 Likes

Definitely, it should otherwise this be reported as a bug :sweat_smile:

To be technically correct (the best kind of correct) these are two separate functions: delete/2 and delete/3. When we say that arity is part of the function name, this is very literal. Elixir makes it possible to not group them though this is this more of a quirk of its syntax (and as mentioned it emits a warning and most people compile with --warnings-as-errors). In Erlang this isn’t even possible:

1> foo("A") -> "You said 'A'"; foo(N) -> "You didn't say 'A'".
ok
2> bar("A", "B") -> "You said 'A'"; bar(N) -> "You didn't say 'A'".
* 1:15: syntax error before: '->'
1 Like

Yes, per the blurb at the top of the moduledoc, it’s three functions:

If I were to, say, rename the current strict parameter to mode :: (:default | :strict | :lax), would that be at all possible to integrate with a default parameter for count :: (:all | non_neg_integer) in a way that doesn’t scare ExDoc so badly?

Which could be grouped as:

def decrease_strict(ms, element, count \\ nil)
def decrease_lax(ms, element, count \\ nil)

I would assume from the context that not passing count deletes all elements, but that’s unintuitive for someone using the library because I wouldn’t expect decreasing without a count to mean deletion. Perhaps making the default argument 1 and allowing it to also be set to :max (which would act as a delete) could be more intuitive? Decreasing the max amount of elements seems semantically intuitive to me (I also thought of :all, but decreasing all elements makes less sense).

Hmm. After much tinkering, I found out that the OP’s function signature could, in fact, be explained to ExDoc, almost unmodified, with the correct arrangement of function heads.

The key was to just go all-in on the Elixir-native “default arguments”, and then add a few careful def for the 3-arity path to match on which argument was actually filled:

@spec declarations
@spec delete(t(e), e) :: t(e) when e: term()
@spec delete(t(e), e, nil) :: t(e) when e: term()
@spec delete(t(e), e, :strict) :: t(e) when e: term()

@spec delete(t_lax(e), e) :: t_lax(e) when e: term()
@spec delete(t_lax(e), e, nil) :: t_lax(e) when e: term()
@spec delete(t_lax(e), e, :lax) :: t_lax(e) when e: term()

@spec delete(t(e), e, :all | non_neg_integer()) :: t(e) when e: term()
@spec delete(t(e), e, :all | non_neg_integer(), nil) :: t(e) when e: term()
@spec delete(t(e), e, :all | non_neg_integer(), :strict) :: t(e) when e: term()

@spec delete(t_lax(e), e, :all | non_neg_integer()) :: t_lax(e) when e: term()
@spec delete(t_lax(e), e, :all | non_neg_integer(), nil) :: t_lax(e) when e: term()
@spec delete(t_lax(e), e, :all | non_neg_integer(), :lax) :: t_lax(e) when e: term()
def delete(ms, element, count \\ :all, strict \\ nil)

def delete(ms, element, strict, nil) when strict === :strict or strict === :lax,
  do: delete(ms, element, :all, strict)

def delete(ms, element, :all, :strict) do
  ...
end

def delete(ms, element, count, :strict) when is_non_neg_integer(count) do
  ...
end

def delete(ms, element, :all, :lax) do
  ...
end

def delete(ms, element, count, :lax) when is_non_neg_integer(count) do
  ...
end

def delete(ms, element, count, nil) when count === :all or is_non_neg_integer(count) do
  ...
end