【问题标题】:Flatten a list of nested maps in Elixir在 Elixir 中展平嵌套地图列表
【发布时间】:2020-08-07 00:43:30
【问题描述】:

我正在尝试展平嵌套地图列表,以便输出是地图列表,然后可以将其插入数据库表中。嵌套地图可以包含地图列表。一般来说,嵌套地图的最小示例是这样的:

  %{
    a: "a",
    b: "b",
    c: [
      %{
        d: "d1",
        e: [%{f: "f1", g: "g1"}],
        h: "h1",
        i: "i1"
      },
      %{
        d: "d2",
        e: [%{f: "f2", g: "g2"}],
        h: "h2",
        i: "i2"
      }
    ]
  }

我要寻找的输出是:


  [
    %{f: "f1", g: "g1", d: "d1", h: "h1", i: "i1", b: "b", a: "a"},
    %{f: "f2", g: "g2", d: "d2", h: "h2", i: "i2", b: "b", a: "a"}
  ]
  

列表的长度等于“终端”映射的数量(即本例中的f 键)。此外,您会注意到在发生嵌套的位置,ce,这些键不是必需的,因此被删除。

我试图递归映射键,但我遇到的问题是输出始终是父映射中键的长度。

任何有关如何解决此问题的帮助或想法将不胜感激。

谢谢!

【问题讨论】:

  • 如果嵌套的map和上一层的map有相同的key怎么办?地图不能有重复的键。
  • 所有键都是唯一的,所以我不必考虑这一点。
  • 'a' 是一个列表,c 的值是一个列表;为什么你希望后者扩大而第一个不扩大?
  • 我想我对 Elixir 中的单引号和双引号感到困惑。 'a' 实际上是一个字符串而不是字符列表,而 c 是一个列表。所以我只是在扩展列表。我已经编辑了问题以澄清这一点。

标签: functional-programming elixir


【解决方案1】:

因为当我们遇到作为值的列表时,我们需要逐字逐句 fork 遍历关卡,所以我们最好的朋友是Task.async_stream/3。一旦我们要懒惰地执行它,所有的内部操作也是懒惰的,直到我们需要终止结果从flatten/1返回它(Enum.to_list/1。)

defmodule Flat do
  @spec flatten(map()) :: [map()]
  def flatten(input),
    do: input |> do_flatten([%{}]) |> Enum.to_list()

  @doc "here we fork it in parallel and collect results"
  defp do_flatten([%{}|_] = input, acc) do
    input
    |> Task.async_stream(&do_flatten(&1, acc))
    |> Stream.flat_map(&elem(&1, 1))
  end

  @doc """
    add `{key, value}` pairs to each list
    in currently accumulated result
  """
  defp do_flatten(%{} = input, acc) do
    Stream.flat_map(acc, fn list ->
      Enum.reduce(input, [list], &do_flatten(&1, &2))
    end)
  end

  @doc "enumerable as value → go for it"
  defp do_flatten({_k, v}, acc) when is_list(v) or is_map(v),
    do: do_flatten(v, acc)

  @doc "the leaf, add to all lists in the accumulator"
  defp do_flatten({k, v}, acc),
    do: Stream.map(acc, &Map.put(&1, k, v))
end
input = %{
  a: "a", b: "b",
  c: [
    %{d: "d1", e: [%{f: "f1", g: "g1"}], h: "h1", i: "i1"},
    %{d: "d2", e: [%{f: "f2", g: "g2"}], h: "h2", i: "i2"}]
}

Flat.flatten()
#⇒ [
#    %{a: "a", b: "b", d: "d1", f: "f1", g: "g1", h: "h1", i: "i1"},
#    %{a: "a", b: "b", d: "d2", f: "f2", g: "g2", h: "h2", i: "i2"}
#  ]

这是一篇博文,以“Wolf, Goat, Cabbage” riddle 为例详细解释了这种技术。

【讨论】:

  • 那是一篇非常及时的博文。感谢您的解决方案,我仍在研究细节。我一直认为延迟加载是性能和内存的解决方案。关于如何解决此类问题以及在解决问题时如何最终调试的任何建议。
  • 延迟处理基本上是关于我们如何处理可枚举的。贪婪操作(来自Enum 模块)将操作应用于可枚举中的所有元素并返回中间结果。 Lazy (Stream) 将每个元素传递给链中的所有操作。在这里它更可取,因为可枚举在操作之间扩展。我们从长度为 1 的列表开始,以任意长度的列表结束。
  • 建议很难 :) 在我意识到我要遵循的道路之前,我绝对不会开始编写代码。我用Access.all/0 尝试过这个,然后意识到它不适合并切换到惰性任务。在任何步骤中,我都会使用IO.inspect/0(在我想要检查之前终止流。)
  • 我很感激。在我看来,这里的关键见解是在列表分叉时需要一个惰性函数调用。这样,您就可以将相同的累加器传递给分叉处的每个分支。仅供参考,我在具有约 7000 个终端节点的地图列表上运行此代码,并且它可以工作,具有任意数量的分支。我现在只是在验证数据!
  • ? 实际上,它并不取决于嵌套级别的深度。一旦它适用于 2 个级别,它适用于 1M,唯一的问题可能是性能。顺便说一句,一个人可能会贪婪地使用Task.async/1Task.yield_many/2 分叉,如此链接下的示例所示,但是当一个人有 7000 个终端节点时,它可能会吞噬整个内存:)
猜你喜欢
  • 2021-06-01
  • 2015-04-18
  • 1970-01-01
  • 2018-07-04
  • 1970-01-01
  • 1970-01-01
  • 2012-03-09
  • 1970-01-01
  • 2015-01-28
相关资源
最近更新 更多