【问题标题】:How to spec a callback with variable number of arguments in Elixir如何在 Elixir 中指定具有可变数量参数的回调
【发布时间】:2020-01-01 22:29:01
【问题描述】:

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

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 做同样的事情。

是否可以在不重新实现行为的情况下正确指定这一点?

【问题讨论】:

    标签: elixir dialyzer


    【解决方案1】:

    我不确定我是否正确理解了问题;我不知道像在 mfa 中那样总是传递 a list 参数会有什么问题。

    不管怎样,对于上述问题,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
    

    【讨论】:

    • 谢谢!因此,如果没有重型机器,一个可变数量参数的规范将无法工作。在这种情况下,按照您的建议,更改实现会更容易。 'after_compile' 的技巧很巧妙!
    • FWIW,我几乎在所有需要行为的地方都执行上述操作,因为它实际上会将警告转变为编译时错误。
    • 我总是用--warnings-as-errors 标志编译。与__after_compile__ 相比,我相信这是一个更简单的解决方案
    • 这取决于:) 当您在自己的代码中定义行为时,当然可以。无论您创建包/库,--warnings-as-errors 都会是用户的善意。
    猜你喜欢
    • 2011-01-07
    • 2011-03-20
    • 2013-03-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-12-24
    • 1970-01-01
    • 2023-03-25
    相关资源
    最近更新 更多