解构 Elixir 地图时对缺失字段的惯用处理?

Idiomatic handling of missing fields when deconstructing an Elixir map?

在地图解构中出现模式匹配错误的情况下,在 Elixir 中获取“默认”值或捕获错误的最佳方法是什么?

iex(1)> %{"a" => a} = %{"a" => 1, "b" => 2}
%{"a" => 1, "b" => 2}
iex(2)> a
1
iex(3)> %{"m" => m} = %{"a" => 1, "b" => 2}

** (MatchError) no match of right hand side value: %{"a" => 1, "b" => 2}

在深层嵌套的情况下也必须干净地工作:

iex(4)> %{"b" => %{"c" => %{"e" => myvar}}} = %{"a" => 1, "b" => %{"c" => %{"d" => 4, "e" => 5}}}
%{"a" => 1, "b" => %{"c" => %{"d" => 4, "e" => 5}}}
iex(5)> myvar
5
iex(6)> %{"b" => %{"c" => %{"e" => myvar}}} = %{"a" => 1, "f" => 6}             

** (MatchError) no match of right hand side value: %{"a" => 1, "f" => 6}

所以在上述情况下,我喜欢 amyvar 回退到默认值,或者以某种干净的方式分支到处理程序函数。如果可能的话,我更喜欢不涉及错误处理程序的解决方案。

我会让其他有更多经验的人来权衡什么更符合习惯,但我通常会通过模块属性提供可选的默认值,然后允许通过合并函数进行覆盖。像这样(使用 Map.merge/2 or Keyword.merge/2 作为关键字列表):

defmodule Something do

  @defaults %{"a" => "alpha", "b" => "beta"}

  def foo(input) do
    input_or_defaults = Map.merge(@defaults, input)
    # ... 
  end
end

如果你的问题真的是深入研究错误捕获,那么这里有一个“隐式救援”的例子,它可以处理没有匹配的情况:

def risky_stuff(input) do
   do_match(input)
rescue e in UndefinedFunctionError ->
  {:error, "Unable to match"}
end

defp do_match(%{"a" => a}), do: "something with a"
defp do_match(%{"b" => b}), do: "something with b"

查看 "implicit try" 以获得对此的一些解释。

不过,我认为您会通过合并找到所需的功能。

显然不能用 match 来完成,因为实际上不可能用 match 语法来适应默认值。

如果您知道地图没有 nil 值,您可能会像 get_in/2 那样做,然后在结果为 nil.

时使用默认值

另外,自己做一个函数也没那么复杂

safe_get_in = fn keys, input, default ->
  Enum.reduce_while(keys, input, fn key, acc ->
    case acc do
      %{^key => level_down} -> {:cont, level_down}
      _ -> {:halt, default}
    end
  end)
end

safe_get_in.(~w|a b c d|, %{"a" => 1, "f" => 6}, 42)
#⇒ 42
safe_get_in.(~w|b c d|, %{
  "a" => 1, "b" => %{"c" => %{"d" => 4, "e" => 5}}}, 42)
#⇒ 4

在不知道它是否存在的情况下获取深层嵌套值的情况并不常见,这就是标准库中没有针对这种情况的实现的原因。

从删除的评论到Aleksei的回答:

yes I can write a function, but I'm looking for an idiomatic answer.

在 Elixir 中惯用的做法是如果匹配失败则失败,或者确保匹配总是成功。

根据上下文,有多种方法可以提供始终匹配的“默认”子句。以下是使用 casewith 和多个函数子句的 3 个示例。

案例:

case value do
  %{"a" => a} -> a
  _ -> "default value"
end

有:

with %{"a" => a} <- value do
  a
else
  _ -> "default value"
end

函数子句:

def default_function_clause(%{"a" => a}), do: a
def default_function_clause(_), do: "default value"

在所有情况下,提供带有 "a" 键的映射将 return 值,否则 "default value".