当你在 Elixir 中只有一个函数定义时,使用守卫是惯用的吗?

Is it idiomatic to use guards when you only have one definition of a function in Elixir?

当你只有一个函数定义时,使用守卫是惯用的吗?

例如

defmodule Math do
    @spec add(integer(), integer()) :: integer()
    def add(a, b) when is_integer(a) and is_integer(b), do: a + b
end

defmodule Math do
    @spec add(integer(), integer()) :: integer()
    def add(a, b), do: a + b
end

哪个是首选?

这取决于您希望参数的具体程度。我在 Oban 这样的开源项目中看到 guard 子句被用在只有一个定义的函数中。例如

  @doc false
  @spec validate!(Keyword.t()) :: :ok
  def validate!(opts) when is_list(opts) do
    Enum.each(opts, &validate_opt!/1)
  end

单子句函数上的这种守卫基本上是对参数类型的断言。在你的例子中,调用函数是这样的:

Math.add(7.2, 3.2)

结果:

** (FunctionClauseError) no function clause matching in Math.add/2

    The following arguments were given to Math.add/2:

        # 1
        7.2

        # 2
        3.2

所以它基本上定义了 +.

的纯整数版本

如果您认为使用不正确的参数调用函数会导致模糊或难以检测到错误,则该机制可能会有用。添加这样的保护子句可以通过指出问题所在的确切调用并可能及早发现错误来简化调试。

是的,即使只有一个函数子句,使用保护子句也是惯用的。为什么?因为使用守卫有助于更好地传达您的意图。

编码很容易;沟通困难

虽然添加 @spec@doc 也有帮助,但守卫明确说明您的函数接受什么,正如其他人指出的那样,FunctionClauseError 错误消息是真的在你面前——我认为它们比当你给你的函数传递一个意想不到的值时可能突然出现的任何奇怪的不可预知的行为更容易调试。

一般来说,守卫和模式匹配的主要好处之一是尽早失败(erlang/elixir 著名的“让它崩溃”哲学的一部分)并完全防止底层逻辑被调用如果输入与您的假设不符。

虽然您的 add 示例一开始并不真正需要它,但执行实际业务逻辑的不那么琐碎的函数(比如来自 phoenix 上下文的 public 函数)可能会受益于守卫/ 更严格的模式。 如果用无效数据调用它,它可能是:

  • 在嵌套调用中失败更深时更难调试
  • 当它不失败时不可预测且有潜在危险

当然,守卫只能进行表面检查并检测一些明显的错误,它们不能代替验证不受信任的用户输入(例如使用 ecto)。

这个section about guards and invalid data描述得很好。这是 来自在 Hex 上编写库的指南,但它明确指出这种哲学也适用于常规的 Elixir 代码。