This is almost what Enum.chunk_by/2
does.
def chunk_by(enumerable, fun)
Splits enumerable on every element for which fun returns a new value.
But chunk_by
won't throw away any elements, so we can combine it with Enum.filter/2
.
list = [1, 2, 3, :stop, 4, 5, 6, :stop, 7, 8, :stop] # analogous to your list
list
|> Enum.chunk_by(&(&1 == :stop))
# at this point, you have [[1,2,3], [:stop], [4,5,6], [:stop], [7,8], [:stop]]
|> Enum.reject(&(&1 == [:stop]))
# here you are: [[1,2,3], [4,5,6], [7,8]]
A second approach would be to use Enum.reduce/3
. Since we build up the accumulator at the front, pushing the first elements we find towards the back, it makes sense to reverse the list before we reduce it. Otherwise we'll end up with a reversed list of reversed lists.
We'll potentially get empty lists, like the final :stop
in our example list. So again, we filter the list at the end.
list
|> Enum.reverse
|> Enum.reduce([[]], fn # note: the accumulator is a nested empty list
:stop, acc -> [[] | acc] # element is the stop word, start a new list
el, [h | t] -> [[el | h] | t] # remember, h is a list, t is list of lists
end)
|> Enum.reject(&Enum.empty?/1)
Finally, let's walk the list ourselves, and build an accumulator. If this reminds you of the reduce
version, it's no coincidence.
defmodule Stopword do
def chunk_on(list, stop \\ :stop) do
list
|> Enum.reverse
|> chunk_on(stop, [[]])
end
defp chunk_on([], _, acc) do
Enum.reject(acc, &Enum.empty?/1)
end
defp chunk_on([stop | t], stop, acc) do
chunk_on(t, stop, [[] | acc])
end
defp chunk_on([el | t], stop, [head_list | tail_lists]) do
chunk_on(t, stop, [[el | head_list] | tail_lists])
end
end
We use the common pattern of a public function that doesn't require users to worry about the accumulator, and passing on the inputs to a private arity+1 function with an accumulator. Since we're building up a list of lists, it's useful to start off the accumulator with an empty list inside it. This way, we don't have to special case when the accumulator is empty.
We reverse the list before we walk it, as we did for reduce
, just as we reject empty lists after we're done. The same reasons apply.
We use pattern matching to identify the stop word. The stop word marks the beginning of a new list, so we add a new, empty list and throw away the stop word.
A regular word is simply put at the front of the first list, in our list of lists. The syntax is a bit unwieldy with all those bars and brackets.