Chunking list based on struct type changing

2020-08-26 11:21发布

问题:

I have a list I want to chunk up based on a transition from struct type B to A. So for example, I have the following:

iex(1)> defmodule A, do: defstruct []
{:module, A ...
iex(2)> defmodule B, do: defstruct []
{:module, B ...
iex(3)> values = [ %A{}, %A{}, %B{}, %B{}, %B{}, %A{}, %A{}, %B{} ]
[%A{}, %A{}, %B{}, %B{}, %B{}, %A{}, %A{}, %B{}]

I want to have that data chunked up into a 2-element list containing:

[ [ %A{}, %A{}, %B{}, %B{}, %B{} ], [ %A{}, %A{}, %B{} ] ]

If the input were to be all A's or all B's initially, the output would be unchanged, since no B->A transition occurred.

I imagine Enum.chunk_by/2 is the way to go, but I'm having trouble figuring out how to maintain the context of the previous element to know when to split.

What does an idiomatic solution to something like this look like?

回答1:

Another alternative is to chunk_by the struct type then do another pass merging the lists (except when the list contains %B{}):

def chunk(structs) do
  structs
  |> Enum.chunk_by(& &1.__struct__)
  |> merge()
end

# Don't merge when current is %B
defp merge([[%B{}|_]=h|t]), do: [h|merge(t)]

# Merge all others
defp merge([curr, next|t]), do: [curr ++ next|merge(t)]

# We are done
defp merge([]), do: []


回答2:

Yet another approach is to use pure recursion:

def collect_chunks([]), do: []
def collect_chunks(list) do
  {chunk, post_chunk} = collect_chunk(list)
  [chunk | collect_chunks(post_chunk)]
end

defp collect_chunk([]), do: {[], []}
defp collect_chunk([%B{} = last_element | [%A{} | _] = post_chunk]), do: {[last_element], post_chunk}
defp collect_chunk([el | rest]) do
  {remaining_chunk, post_chunk} = collect_chunk(rest)
  {[el | remaining_chunk], post_chunk}
end


回答3:

Enum.chunk_by/2 currently does not provide access to the previous element so we can not use Enum.chunk_by/2 in this case. We will have to fallback to reduce/3

Of all the Enum functions, reduce/3 is the most flexible and is used internally by most, if not all, of the Enum functions.

Below is one way to go about producing the output you want, given the values [ %A{}, %A{}, %B{}, %B{}, %B{}, %A{}, %A{}, %B{} ]:

  values
  |> Enum.reduce([[]], fn (elem, acc) ->
    prev_list = List.first(acc)         
    prev_elem = List.first(prev_list)
    b_changed_to_a? = fn -> prev_elem.__struct__ == B && elem.__struct__ == A end

    if is_nil(prev_elem) || !b_changed_to_a?.() do
      List.replace_at(acc, 0, [elem|prev_list])
    else
      [[elem]|acc]      
    end
  end)
  |> Enum.map(&Enum.reverse/1)
  |> Enum.reverse

Notice that I always prepend an element to a list. This is because appending to a list in Elixir is an expensive operation.

Hope this solution helps!



标签: elixir