如何在 Elixir 中转置列表?

How do I tranpose a list in Elixir?

我希望在 Elixir 中有效地转置一个(大)列表列表,其中每个子列表具有相同数量的标量元素。所以从这个:

iex(1)> x = [[1, 2], [3, 4], [5, 6]]
[[1, 2], [3, 4], [5, 6]]

我想讲这个:

[[1, 3, 5], [2, 4, 6]]

在 Python 中,我会使用 "splat" 运算符 *zip:

In [1]: x = [[1, 2], [3, 4], [5, 6]]
In [2]: x
Out[2]: [[1, 2], [3, 4], [5, 6]]
In [3]: zip(*x)
Out[3]: [(1, 3, 5), (2, 4, 6)]

这完全等同于转置矩阵。

Elixir 中的等价物是什么?我需要对来自 CSV 的大量列表执行此操作。

奖励:地图类似,例如:

iex(1)> %{"a1" => %{"d1" => 1, "d2" => 2}, "a2" => %{"d1" => 3, "d2" => 4}, "a3" => %{"d1" => 5, "d2" => 6}}
%{
  "a1" => %{"d1" => 1, "d2" => 2},
  "a2" => %{"d1" => 3, "d2" => 4},
  "a3" => %{"d1" => 5, "d2" => 6}
}

变成

%{
  "d1" => %{"a1" => 1, "a2" => 3, "a3" => 5},
  "d2" => %{"a1" => 2, "a2" => 4, "a3" => 6}
}

对于列表,您有 Enum.zip/2 and Stream.zip/1

iex> Enum.zip([[1, 2], [3, 4], [5, 6]])
[{1, 3, 5}, {2, 4, 6}]

重新排列地图有点棘手。我敢肯定,有一种更聪明、更好的方法可以做到这一点,但您可以将其分解为平面可枚举,然后将其重新组合成地图。在这里,我会考虑使用理解。

iex> input = %{
...>   "a1" => %{"d1" => 1, "d2" => 2},
...>   "a2" => %{"d1" => 3, "d2" => 4},
...>   "a3" => %{"d1" => 5, "d2" => 6}
...> }
iex> for {outer, map} <- input, {inner, value} <- map, reduce: %{} do
...>   acc -> Map.update(acc, inner, %{outer => value}, &Map.put(&1, outer, value))
...> end
%{
  "d1" => %{"a1" => 1, "a2" => 3, "a3" => 5}, 
  "d2" => %{"a1" => 2, "a2" => 4, "a3" => 6}
}

对于列表:

正如 BrettBeatty 提到的,您可以按如下方式使用 zip/1:

iex(1)> x = [[1, 2], [3, 4], [5, 6]]
iex(2)> x |> Enum.zip |>  Enum.map(&(Tuple.to_list(&1)))

还有一种递归的方法:

defmodule E do
  def transpose([]), do: []
  def transpose([[]|_]), do: []
  def transpose(list) do
    [Enum.map(list, &hd/1) | transpose(Enum.map(list, &tl/1))]
  end
end

iex(3)> x |> E.transpose()

Brett 的回答完全正确;我只是想分享另一种使用 Kernel.put_in/3 with Access.key/2 转置地图的方法。这种方法很容易可扩展到转置任何深度的地图。

Enum.reduce(map, %{}, fn {k, vs}, acc ->
  Enum.reduce(vs, acc, fn {ik, iv}, acc ->
    put_in(acc, [Access.key(ik, %{}), k], iv)
  end)
end)

#⇒ %{
#    "d1" => %{"a1" => 1, "a2" => 3, "a3" => 5},
#    "d2" => %{"a1" => 2, "a2" => 4, "a3" => 6}
#  }

Access.key/2 接受默认值,允许您将元素埋入所需的深度。

put_in(%{}, [
  Access.key(:k1, %{}),
  Access.key(:k2, %{}),
  Access.key(:k3, %{})
], 42)
#⇒ %{k1: %{k2: %{k3: 42}}}

基于

Access 的数组转置解决方案也是可能的,但是需要预先知道结果列表的长度。

len = list |> Enum.map(&Enum.count/1) |> Enum.max()
Enum.map(0..len-1, fn idx ->
  Enum.map(list, &get_in(&1, [Access.at(idx)]))
end)
#⇒ [[1, 3, 5], [2, 4, 6]]

Enum.zip/1 不同,它将保留所有元素,将 nil 放在输入短于最大长度的位置。