Q:

How to spec a callback with variable number of arguments in Elixir

I have a behaviour that wraps any function.
defmodule MyBehaviour do
  @callback do_run( ? ) :: ? #the ? means I don't know what goes here
  defmacro __using__(_) do
    quote location: :keep do
      @behaviour MyBehaviour
      def run, do: MyBehaviour.run(__MODULE__, [])
      def run(do_run_args), do: MyBehaviour.run(__MODULE__, do_run_args)
    end
  end

  def run(module, do_run_args) do
    do_run_fn = fn ->
      apply(module, :do_run, do_run_args)
    end

    # execute do_run in a transaction, with some other goodies
  end
end

defmodule Implementation do
  use MyBehaviour

  def do_run(arg1), do: :ok
end

Implemenation.run([:arg1])

The idea is that by implementing MyBehaviour, the module Implementation will have run([:arg1]) function that will call do_run(:arg1).
How do I write @callback specification for a function with a variable number of arguments?
I thought that @callback do_run(...) :: any() would work, but Dialyzer gives me an error that Undefined callback function do_run/1, so I assume ... means any argument but not zero arguments.
In reality, I have only two cases: for zero and one arg.
I thought about overloading the spec like this:
@callback do_run() :: any()
@callback do_run(any()) :: any()

but that requires two do_run functions because the same name and different arity are two separate functions in the Erlang world.
If I make it @optional_callback there is a possibility that neither of them will get implemented.
@type allows specifying functions of any arity like this (... -> any()) so I would imagine that it should be possible to do the same with @callback.
Is it possible to spec this properly without reimplementing the behaviour?

A:

I am not sure I understood the problem correctly; I do not follow what would be wrong with always passing a list of arguments as we do in mfa.
Anyway, for the problem as it’s stated, Module.__after_compile__/2 callback and @optional_callbacks are your friends.
defmodule MyBehaviour do
  @callback do_run() :: :ok
  @callback do_run(args :: any()) :: :ok
  @optional_callbacks do_run: 0, do_run: 1
  defmacro __using__(_) do
    quote location: :keep do
      @behaviour MyBehaviour
      @after_compile MyBehaviour

      def run(), do: MyBehaviour.run(__MODULE__)
      def run(do_run_args), do: MyBehaviour.run(__MODULE__, do_run_args)
    end
  end

  def run(module),
    do: fn -> apply(module, :do_run, []) end

  def run(module, do_run_args),
    do: fn -> apply(module, :do_run, do_run_args) end

  def __after_compile__(env, _bytecode) do
    :functions
    |> env.module.__info__()
    |> Keyword.get_values(:do_run)
    |> case do
      [] -> raise "One of `do_run/0` _or_ `do_run/1` is required"
      [0] -> :ok # without args
      [1] -> :ok # with args
      [_] -> raise "Arity `0` _or_ `1` please"
      [_|_]  -> raise "Either `do_run/0` _or_ `do_run/1` please"
    end
  end    
end

And use it as:
defmodule Ok0 do
  use MyBehaviour
  def do_run(), do: :ok
end
Ok0.run()

defmodule Ok1 do
  use MyBehaviour
  def do_run(arg1), do: :ok
end
Ok1.run([:arg1])

defmodule KoNone do
  use MyBehaviour
end
#⇒ ** (RuntimeError) One of `do_run/0` _or_ `do_run/1` is required

defmodule KoBoth do
  use MyBehaviour
  def do_run(), do: :ok
  def do_run(arg1), do: :ok
end
#⇒ ** (RuntimeError) Either `do_run/0` _or_ `do_run/1` please

defmodule KoArity do
  use MyBehaviour
  def do_run(arg1, arg2), do: :ok
end
#⇒ ** (RuntimeError) Arity `0` _or_ `1` please

