如何在 Elixir 中使用可变数量的参数指定回调

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

我有一个包装任何函数的行为。

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])

想法是,通过实施 MyBehaviour,模块 Implementation 将具有 run([:arg1]) 函数,该函数将调用 do_run(:arg1).

如何为参数数量可变的函数编写 @callback 规范?

我认为 @callback do_run(...) :: any() 会起作用,但 Dialyzer 给我一个错误 Undefined callback function do_run/1,所以我假设 ... 表示任何参数但不是零参数。

实际上,我只有两种情况:零和一个 arg。 我考虑过像这样重载规范:

@callback do_run() :: any()
@callback do_run(any()) :: any()

但这需要两个 do_run 函数,因为在 Erlang 世界中相同的名称和不同的元数是两个独立的函数。

如果我做到了 @optional_callback 有可能两者都不会实施。

@type 允许像这样指定任何参数的函数 (... -> any()) 所以我想应该可以用 @callback.

做同样的事情

是否可以在不重新实现行为的情况下正确规范此行为?

我不确定我是否正确理解了问题;我不理解总是传递 a list 参数会有什么问题,就像我们在 mfa.

中所做的那样

总之,对于上面提到的问题,Module.__after_compile__/2回调和@optional_callbacks是你的朋友

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

并将其用作:

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