Flatten multi-dimensional to 2-dimensional

For my first approach to AoC day 7, I needed a way to flatten a multi-dimensional list, down to just 2-dimensional. (Before you say anything: Now that I solved it, I already have a better approach that won’t need this.)

List.flatten/1 goes too far; what I needed was this (2-element example shown, but I tested this and it works for larger sizes/depths too):

  @doc ~S"""
  ## Examples
      iex> flatten_2d([[[[:a, :b],[:c, :d]],[[:e, :f],[:g, :h]]],[[[:i, :j],[:k, :l]],[[:m, :n],[:o, :p]]]])
      [[:a, :b], [:c, :d], [:e, :f], [:g, :h], [:i, :j], [:k, :l], [:m, :n], [:o, :p]]
  """
  @spec flatten_2d([[]]) :: [[]]
  def flatten_2d([h | t]) when is_list(h) do
    if is_list(List.first(h)) do
      [h | t]
      |> Enum.map(&flatten_2d/1)
      |> Enum.concat()
    else
      [h | t]
    end
  end

I couldn’t find anything in List or Enum that seemed to do this, so I wrote the above. Is there an existing or better way to do this?

I believe simply calling Enum.flat_map should do it. Each item in the top level list is a list, and those will be essentially concatenated, and then their contents left alone.

3 Likes

This is not an answer to your question of “is there something in List or Enum” and Ben’s answer is what I would do, but for the AoC its fun to not reach for the standard library:

defmodule MapFlatter do
  def flatten(input) do
    do_flatten(input, [])
  end

  def do_flatten([a, b] = input, acc)
    when not is_list(a) and not is_list(b), do: [input | acc]

  def do_flatten([], acc), do: acc

  def do_flatten([h | t], acc) do
    do_flatten(h, acc) ++ do_flatten(t, acc) ++ acc
  end
end

result = MapFlatter.flatten([[[[:a, :b],[:c, :d]],[[:e, :f],[:g, :h]]],[[[:i, :j],[:k, :l]],[[:m, :n],[:o, :p]]]])
IO.inspect result
[[:a, :b], [:c, :d], [:e, :f], [:g, :h], [:i, :j], [:k, :l], [:m, :n], [:o, :p]]
2 Likes

I agree! I spent more time on this function than I should have, but it’s all about learning, practicing, and improving. I don’t do AoC for speed.

Thank you both for your replies; @mpope it was helpful to review your solution, I hadn’t seen this use of a pattern match in the function argument before, so you can use both the whole and the parts in the function. I’ll remember that trick for the future.

def do_flatten([a, b] = input, acc)

Using guards and Enum.flat_map/2 I got mine trimmed down to this:

  def flatten_2d(list) when is_list(hd(list)) and is_list(hd(hd(list))) do
    Enum.flat_map(list, &flatten_2d/1)
  end
  def flatten_2d(list) when is_list(list), do: list

Thanks again!

2 Likes