"with" 运算符中的逻辑条件不起作用

A logical condiditon in "with" operator doesn't work

我有这个代码:

  def edit(conn, params) do
    with m1 <- Repo.get(Model1, params["model1_id"]),
      m2 <- Repo.get(Model2, params["model2_id"]),
      !is_nil(m1) and !is_nil(m2)
    do
      # 1
      res = !is_nil(m1) and !is_nil(m2)
      IO.puts("***** res: #{res}")                              # ===> false

      IO.puts("***** m1: #{Kernel.inspect(m1)}")                # ===> prints a struct
      IO.puts("***** m1 is_nil: #{is_nil(m1)}")                 # ===> false

      IO.puts("***** m2: #{Kernel.inspect(m2)}")                # ===> nil
      IO.puts("***** m2 is_nil: #{is_nil(m2)}")                 # ===> true

    else
      #2
      _ -> raise ArgumentError, "not found"
    end
  end

即使 m2 为零,流程 #1 也会执行。怎么可能?如何解决? 目标 - 确保 m1 和 m2 不为零,然后执行流程 #1。

Kernel.SpecialtForms.with/1 “early returns” 当且仅当子句中没有匹配时。

在第三个子句中,您有 !is_nil(m1) and !is_nil(m2),大致意思是 _ <- !is_nil(m1) and !is_nil(m2),无论如何它都匹配。要实现您想要的效果,您需要明确使用适当的 with 子句和 <-:

with m1 <- Repo.get(Model1, params["model1_id"]),
     m2 <- Repo.get(Model2, params["model2_id"]),
     true <- !is_nil(m1) and !is_nil(m2), do: ...

更自然的做法是使用适当的守卫来早期 return 错误:

with m1 when not is_nil(m1) <- Repo.get(Model1, params["model1_id"]),
     m2 when not is_nil(m2) <- Repo.get(Model2, params["model2_id"]),
       do: ...

其实这里不需要with/1。这将是完美的(感谢 nil 成为 falsey):

if Repo.get(Model1, params["model1_id"]) &&
   Repo.get(Model2, params["model2_id"]), do: ...

with

with 表达式严格用于模式匹配。它不是 if-else 条件的 "chainable replacement"。

基本上 with 会遍历所有子句并尝试将它们与 <- 箭头的左侧进行模式匹配。它只会在第一个模式匹配失败(不匹配)时执行 error 子句之一。

你的代码有问题

您在 with 中的第三行是 !is_nil(m1) and !is_nil(m2),即使表达式本身等于 false.

,它也始终成功进行模式匹配

修复

要使代码执行您实际需要的操作,您应该在第三行添加一个左侧,以便强制进行模式匹配:

with m1 <- Repo.get(Model1, params["model1_id"]),
      m2 <- Repo.get(Model2, params["model2_id"]),
      {false, false} <- {is_nil(m1), is_nil(m2)} do
 ...

地道灵药

为了使代码更符合 Elixir 的习惯,您还可以使用 Guards,其中 is_nil 是允许的。 这将使您的代码看起来像:

with m1 when not is_nil(m1) <- Repo.get(Model1, params["model1_id"]),
      m2 when not is_nil(m2) <- Repo.get(Model2, params["model2_id"]) do
 ...

更好的可读性

最后一个提示是始终关注可读性。您正在编写供人们阅读的代码,因此一行中发生的事情越少通常越容易阅读。

您的代码将更具可读性:

m1 = Repo.get(Model1, params["model1_id"])
m2 = Repo.get(Model2, params["model2_id"])

with m1 when not is_nil(m1) <- m1,
      m2 when not is_nil(m2) <- m2 do
 ...

你真的需要 with 吗?

你的 with 除了确保 m1m2 不是 nil 之外什么也没做。这也可以使用 caseif 轻松完成,因为您实际上不需要任何模式匹配:

m1 = Repo.get(Model1, params["model1_id"])
m2 = Repo.get(Model2, params["model2_id"])

if !is_nil(m1) && !is_nil(m2) do
 ...

我发现自己只在特定情况下使用 with,在那些情况下它确实有帮助,主要是当它是一组类似于 "pipe" 的操作时,例如,您需要以前的结果下一步中的步骤,但实际上是异构的,您没有或没有必要创建一些令牌结构来保存转换和错误(类似于外变更集)。

在这些情况下,如果知道需要失败的步骤,我发现将 with 语句包装在标记的元组上会有所帮助,因为这样您就可以匹配失败的特定标记。除此之外,您并没有真正使用惯用代码,因为您只是将 with 用作赋值表达式,如果您使用模式匹配,那么它会变得更具可读性,并且在我看来更加惯用。对于您的示例,这意味着:

with {_, %Model1{} = m1} <- {Model1, Repo.get(Model1, params["model1_id"])},
     {_, %Model2{} = m2} <- {Model2, Repo.get(Model2, params["model2_id"])}
       do
          # we have both m1 and m2 and they are respectively instances of Model1 and Model2
          # do something with them
          {:ok, {m1, m2}}
else
     {Model1, _} -> 
            #failed fetching Model1
            {:error, :no_model1}
     {Model2, _} ->
            #failed fetching Model2
            {:error, :no_model2}
end

模式完全匹配你想要的结构,并且知道 Repo.get 将 return 要么是模式结构,要么是 nil,这样你就不必检查它是否为 nil,如果它不是模式结构,它将为零(除非你使用 Repo.get 和 select 子句,你 return 其他的东西)。

请记住,访问 params["some_key"] 可能 return nil 并且在尝试执行 Repo.get 时会抛出异常,因此您可以再添加两个具有条件的语句对于 id 并且还允许您 return id 以防找不到它们,假设 id 是数字 id(如果二进制将 is_integer 更改为 is_binary):

with {_, id1} when is_integer(id1) <- {:id1, Map.get(params, "model1_id")},
     {_, id2} when is_integer(id2) <- {:id2, Map.get(params, "model2_id")},
     {_, _, %Model1{} = m1} <- {Model1, id1, Repo.get(Model1, id1)},
     {_, _, %Model2{} = m2} <- {Model2, id2, Repo.get(Model2, id2)}
       do
          # we have both m1 and m2 and they are respectively instances of Model1 and Model2
          # do something with them
          {:ok, {m1, m2}}
else
     {id_type, id_value} when id_type in [:id1, :id2] ->
            # one of the id params wasn't an integer
            {:error, {:unexpected_id, id_type, id_value}}
     {Model1, id, _} -> 
            # failed fetching Model1
            {:error, {:no_model1, id}}
     {Model2, id, _} ->
            # failed fetching Model2
            {:error, {:no_model2, id}}
end

可能需要对参数进行一些更好的处理,比如在执行到这个阶段之前验证它们,如果完成了,那么您可能可以使用 case 语句,因为它只有 2 "cases":

case Repo.get(Model1, valid_id_1) do
    %Model1{} = model1 ->

       case Repo.get(Model2, valid_id_2) do
           %Model2{} = model2 -> {:ok, {model1, model2}}
           nil -> {:error, {:no_model2, valid_id_2}}
       end

    nil -> {:error, {:no_model1, valid_id_1}}
end