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?
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