简体   繁体   中英

How do I tranpose a list in Elixir?

I wish efficiently to transpose a (large) list of lists in Elixir, where each sublist has the same number of scalar elements. So from this:

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

I want to get to this:

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

In Python I would use the "splat" operator * with 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)]

This is exactly equivalent to tranposing a matrix.

What is the equivalent in Elixir? I need to do this for large lists of lists coming from CSVs.

Bonus: similar for maps eg:

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}
}

becomes

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

For lists you have Enum.zip/2 and Stream.zip/1

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

Rearranging the map gets a little more tricky. I'm sure there's a much cleverer, better way of doing it, but you could break it into a flat enumerable then form it back into a map. And here, I would look at using a comprehension.

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}
}

For lists:

As BrettBeatty mentioned, you can use zip/1 as follows:

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

There is also a recursive way to do it as well:

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's answer is absolutely correct; I just wanted to share another approach for transposing maps using Kernel.put_in/3 with Access.key/2 . This approach is easily extendable to transpose maps of any depth.

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 accepts a default value, allowing you to bury an element as deep as needed.

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

Access -based solution for transposing the array is also possible, but there is a need to know the length of the resulting list upfront.

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]]

Unlike Enum.zip/1 , it'll preserve all the elements, putting nil s where the input is shorter than max length.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM