【问题标题】:How can I return the first truthy value in Elixir?如何返回 Elixir 中的第一个真实值?
【发布时间】:2021-12-05 23:34:37
【问题描述】:

我正在阅读教科书使用 Elixir 学习函数式编程,我对示例问题的一些给出的答案不满意。问题的中心是检查井字游戏的获胜者,但我会尝试将其归结为本质。假设我们要检查一些值元组的有效性并返回一些东西,例如

iex(1)> some_function({:some, :list, :of, :values})
{:ok}
iex(2)> some_function({:some, :other, :list, :of, :values})
{:error}

教科书的解决方案是完全匹配允许的值:

def some_function({_, _, :of, _}), do: {:ok}
def some_function(_everything_else), do: {:error}

但是,我不喜欢这种解决方案,因为在大量案例中它变得非常重复。我试图更直接地对逻辑进行编码,但我想不出一种惯用的方法来做到这一点。我试过了:

def some_function(input) do
  test1(input) or
  test2(input) or
  {:error}
end

def test1({:some, :case}), do: false
def test1({:some, :other, :case}), do: {:ok}  

def test2({:some, :case}), do: false
def test2({:some, :other, :case}), do: {:ok}

这在所有测试都失败时有效,但是当其中任何一个测试通过时,您会收到一个错误,您无法将 {:ok} 与布尔值进行比较。虽然我意识到这很可怕,但我最终做的是

def some_function(input) do
  cond do
    test1(input) -> test1(input)
    test2(input) -> test2(input)
    fail(input) -> {:error}
  end
end

def test1({:some, :case}), do: false
def test1({:some, :other, :case}), do: {:ok}  

def test2({:some, :case}), do: false
def test2({:some, :other, :case}), do: {:ok}

def fail(_input), do: true

从黑盒的角度来看,这实际上表现得非常完美,但是代码带有重复性(尤其是每个测试必须评估两次才能确定真实性并捕获值)。给定一系列要评估的测试用例,是否有一种惯用的方式返回第一个真值?我精通 python 和 C,但我试图避免使用 if 并以“功能”风格进行。

【问题讨论】:

  • “我不喜欢这个解决方案,因为在很多情况下它会变得非常重复” -> 如果它清晰、简单且中肯,那么重复没有错。例如,我认为代表有效获胜状态的函数子句列表非常清晰。
  • @AdamMillerchip 我想这是一个公平的对比。我内心的程序员声音说这太不雅了,但鉴于函数式代码(至少对我而言)比命令式代码更难以推理,也许我应该在“明确和清晰”方面犯错更多" 而不是 "优雅"

标签: functional-programming elixir pattern-matching


【解决方案1】:
  1. or/2||/2 不同,它需要 boolean 参数,而后者将接受任何值,将 nilfalse 视为 falsey 并将其他所有值视为 truthy 值。
  2. 在绝大多数情况下,单元素元组有点意义,使用原子本身 ({:ok} → :ok.)
  3. Enum.find/3 需要一个可枚举的元组,但我们有 Tuple.to_list/1 并且可以将输入转换为列表并调用 Enum.find/3
  4. 正如 Adam 在 cmets 中所说,模式匹配解决方案简洁明了,在这种情况下效果很好。

【讨论】:

  • 感谢您的帮助! 100% ||/2 做到了我想要的。
【解决方案2】:

模式匹配是惯用的,但是,正如您所指出的,当应用于大量案例时,它可能会很庞大,如果您在运行时之前不知道需要匹配的内容,它可能无法正常工作。给定的解决方案要求元组在特定索引处具有特定成员,而您似乎更倾向于开放式解决方案。

作为其他人建议的变体,您可以尝试使用 Enum.member?/2 的方法,例如测试元组是否包含您正在寻找的东西的东西:

iex> {:some, :list, :of, :values}
|> Tuple.to_list()
|> Enum.member?(:of)
true

或者,如果您想检查元组是否包含任何可接受值的列表,您可以使用Enum.any?/2,例如通过在保护子句中定义可接受值的列表

iex> {:some, :list, :of, :values}
|> Tuple.to_list()
|> Enum.any?(fn
  x when x in [:of] -> true
  _ -> false
end)

true

【讨论】:

  • 啊,感谢您的帮助,但提出的问题是关于在井字游戏中找到获胜者,我试图“简化”,但给出了一个关于在列表中查找元素以使其成功的示例问题更容易,但也许我只是让它更混乱:)
【解决方案3】:

出于好奇,这里有一个带有一点元编程的高性能解决方案,它为所有可能的元组生成所有子句(最大元组大小作为参数给出)

defmodule Checker do
  defmacro __using__(opts) do
    total = Keyword.get(opts, :count, 8)

    for count <- 1..total, place <- 1..count do
      pre = for _ <- 1..place - 1//1, do: Macro.var(:_, nil)
      post = for _ <- place + 1..count//1, do: Macro.var(:_, nil)

      quote do
        def member?(what,
          {unquote_splicing(pre), what, unquote_splicing(post)}
        ), do: true
      end
    end ++ [quote do: (def member?(_, _), do: false)]
  end
end

defmodule Test do
  use Checker, count: 4
end

测试如下:

Test.member? :ok, {:ok}
#⇒ true
Test.member? :ok, {42, true, :ok, :foo}
#⇒ true
Test.member? :ok, {42, true, :foo}
#⇒ false

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-12-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-09-23
    相关资源
    最近更新 更多