【问题标题】:How do I map and group_by at the same time?如何同时映射和 group_by?
【发布时间】:2019-12-14 05:36:51
【问题描述】:

例如,假设我有一个可枚举的 collection{first, second}。使用

对这些对进行分组
Enum.group_by(collection, fn {first, second} -> first end)

将产生一个Map,其密钥由传递的匿名函数确定。它的值是对的集合。 但是,我希望它的值包含该对的 second 元素。


一般来说,给定一个可枚举,我想分组提供一个键提取器一个值映射器,这样我就可以确定将什么放入结果 Map 的值中。即,我想要类似的东西

map_group_by(
  collection,
  fn {_first, second} -> second end,
  fn {first, _second} -> first end
)

collection 的值在分组之前被映射,但键映射器仍对原始元素进行操作。

标准库中有这样的功能吗?如果没有,实现这一目标最惯用的方法是什么?


我知道我可以做类似的事情

Enum.reduce(
  collection,
  %{},
  fn({key, value}, acc) -> Dict.update(acc, key, [value], &([value | &1])) end
)

但这看起来很笨拙,并且会先发制人地创建[value] 列表(这是真的吗?)。有没有更好的既简洁又高效的方法?

【问题讨论】:

    标签: elixir


    【解决方案1】:

    从 Elixir 1.3 开始,现在有 Enum.group_by/3 接受 mapper_fun 参数,这正好解决了这个问题:

    Enum.group_by(enumerable, &elem(&1, 0), &elem(&1, 1))
    

    过时的答案:

    目前,标准库中没有这样的功能。我最终使用了这个:

    def map_group_by(enumerable, value_mapper, key_extractor) do
      Enum.reduce(Enum.reverse(enumerable), %{}, fn(entry, categories) ->
        value = value_mapper.(entry)
        Map.update(categories, key_extractor.(entry), [value], &[value | &1])
      end)
    end
    

    (对于我的示例)然后可以这样调用:

    map_group_by(
      collection,
      fn {_, second} -> second end,
      fn {first, _} -> first end
    )
    

    改编自标准库的Enum.group_by。 关于[value]:我不知道编译器能优化什么,不能优化什么,但至少Enum.group_by 也是这样做的。

    注意Enum.reverse 调用,这不在我的问题示例中。这可确保元素顺序保留在结果值列表中。如果您不需要保留该订单(就像我在我的情况下所做的那样,无论如何我只想从结果中取样),它可以被删除。

    【讨论】:

      【解决方案2】:

      真实答案

      从 Elixir 1.3 开始,现在有 Enum.group_by/3,其第三个参数是一个映射到键上的函数。


      过时的答案

      但我会给你我的解决方案:

      首先,重要的是要注意,正如您在 Elixir 文档中看到的那样,元组列表与键值列表相同:

      iex> list = [{:a, 1}, {:b, 2}]
      [a: 1, b: 2]
      iex> list == [a: 1, b: 2]
      true
      

      因此,考虑到这一点,很容易在其中使用Enum.map

      这确实可以通过两次,但它看起来比你拥有的要干净一点:

      defmodule EnumHelpers do
        def map_col(lst) do
          lst
          |> Enum.group_by(fn {x, _} -> x end)
          |> Enum.map(fn {x, y} -> {x, Dict.values y} end)
        end
      end
      
      IO.inspect EnumHelpers.map_col([a: 2, a: 3, b: 3])
      

      将打印出来:

      [a: [3, 2], b: [3]]
      

      编辑:更快的版本

      defmodule EnumHelpers do
      
        defp group_one({key, val}, categories) do
          Dict.update(categories, key, [val], &[val|&1])
        end
      
        def map_col_fast(coll) do
          Enum.reduce(coll, %{}, &group_one/2)
        end
      end
      
      IO.inspect EnumHelpers.map_col_fast([a: 2, a: 3, b: 3])
      

      【讨论】:

      • 为了澄清,这不能使用Map.values,因为它不适用于元组列表而不是真正的“地图”。
      • 感谢您的回复,但这不是我想要的:正如您所指出的,这是第二次通过,但更重要的是,它还创建了一个中间集合。因此,它不必要地低效。
      • @user4235730,我添加了一个适合您的版本。但是请注意,Erlang VM 从不修改内存,因此无论如何它都会创建中间集合,尽管这对于不可变值来说不是问题,因为它可以在某种程度上引用旧版本的未更改部分(如果它像 Clojure 一样工作)。
      • 我所说的中间集合的意思是,您的第一个解决方案创建了一个与最终结果具有不同值的附加映射,这意味着(据我所知)没有结构共享开。
      猜你喜欢
      • 2018-03-27
      • 2012-04-06
      • 1970-01-01
      • 1970-01-01
      • 2020-01-05
      • 2016-03-27
      • 1970-01-01
      相关资源
      最近更新 更多