"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
除了确保 m1
和 m2
不是 nil
之外什么也没做。这也可以使用 case
或 if
轻松完成,因为您实际上不需要任何模式匹配:
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
我有这个代码:
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
除了确保 m1
和 m2
不是 nil
之外什么也没做。这也可以使用 case
或 if
轻松完成,因为您实际上不需要任何模式匹配:
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